add local install script
This commit is contained in:
5
install.sh
Executable file
5
install.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
npm run build
|
||||||
|
ssh air rm -rf /pkg/gnu/unitprint/web-app/dist
|
||||||
|
rsync -avz dist/ air:/pkg/gnu/unitprint/web-app/dist/
|
||||||
|
ssh air nginx -s reload
|
||||||
@ -2,15 +2,10 @@
|
|||||||
<script>
|
<script>
|
||||||
let { store, onBack = () => {} } = $props();
|
let { store, onBack = () => {} } = $props();
|
||||||
|
|
||||||
const currentStatus = $derived.by(() => {
|
function norm(v) {
|
||||||
if (!store.job) return 'idle';
|
if (!v) return '';
|
||||||
|
return String(v).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
||||||
const f = store.job.files?.find((x) => x.status === 'printing');
|
}
|
||||||
if (f) return 'printing';
|
|
||||||
|
|
||||||
const q = store.job.files?.find((x) => x.status === 'queued');
|
|
||||||
return q ? 'queued' : store.job.phase;
|
|
||||||
});
|
|
||||||
|
|
||||||
const OFFICE_EXT = ['DOC', 'DOCX', 'ODT', 'XLS', 'XLSX', 'ODS', 'PPT', 'PPTX', 'ODP'];
|
const OFFICE_EXT = ['DOC', 'DOCX', 'ODT', 'XLS', 'XLSX', 'ODS', 'PPT', 'PPTX', 'ODP'];
|
||||||
|
|
||||||
@ -31,116 +26,76 @@
|
|||||||
return OFFICE_EXT.includes(e) ? e : null;
|
return OFFICE_EXT.includes(e) ? e : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jobPhase = $derived(norm(store.job?.phase));
|
||||||
|
const jobFiles = $derived(store.job?.files ?? []);
|
||||||
|
|
||||||
|
const currentStatus = $derived.by(() => {
|
||||||
|
if (!store.job) return 'idle';
|
||||||
|
|
||||||
|
if (jobFiles.some((f) => norm(f.status) === 'printing')) {
|
||||||
|
return 'printing';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobFiles.some((f) => norm(f.status) === 'queued')) {
|
||||||
|
return 'queued';
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobPhase;
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!store.jobActive) return;
|
if (!store.jobActive) return;
|
||||||
const int = setInterval(() => store.refreshJob(), 1000);
|
|
||||||
|
const int = setInterval(() => {
|
||||||
|
store.refreshJob();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(int);
|
return () => clearInterval(int);
|
||||||
});
|
});
|
||||||
|
|
||||||
const canCancel = $derived(
|
const canCancel = $derived(
|
||||||
store.job?.files?.some((f) => f.status === 'queued' || f.status === 'printing') ?? false
|
jobFiles.some((f) => ['queued', 'printing'].includes(norm(f.status)))
|
||||||
);
|
|
||||||
|
|
||||||
const jobPhase = $derived(store.job?.phase);
|
|
||||||
const jobFiles = $derived(store.job?.files ?? []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Двусторонняя печать определяется по настройкам файлов,
|
|
||||||
* которые всё ещё лежат в store.files после отправки задания.
|
|
||||||
*/
|
|
||||||
const isDuplex = $derived(
|
|
||||||
store.files.some((f) => f.sides === 'Двусторонняя')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Текущие запросы сервера на пользовательские действия.
|
* Явные состояния, при которых нужны пользовательские кнопки.
|
||||||
|
* Здесь больше не завязываемся на hasPrintedFiles / firstSideFinished.
|
||||||
*/
|
*/
|
||||||
const hasClearPhase = $derived(jobPhase === 'awaiting_clear_output');
|
const showStart = $derived(
|
||||||
const hasFlip = $derived(
|
store.jobActive && jobPhase === 'waiting_start'
|
||||||
jobPhase === 'awaiting_flip' ||
|
|
||||||
jobFiles.some((f) => f.status === 'awaiting_flip')
|
|
||||||
);
|
|
||||||
const hasPickup = $derived(
|
|
||||||
jobPhase === 'awaiting_pickup' ||
|
|
||||||
jobFiles.some((f) => f.status === 'awaiting_pickup')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
const showClearOutput = $derived(
|
||||||
* Проверяем, есть ли уже напечатанные или отправленные файлы.
|
store.jobActive &&
|
||||||
* Если нет — значит это первая печать и лоток выдачи принтера пуст.
|
(
|
||||||
*/
|
showStart ||
|
||||||
const hasPrintedFiles = $derived(
|
jobPhase === 'awaiting_clear_output' ||
|
||||||
store.job?.files?.some(f => ['done', 'sent', 'awaiting_flip', 'awaiting_pickup'].includes(f.status)) ?? false
|
jobFiles.some((f) => norm(f.status) === 'awaiting_clear_output')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Нам важно запомнить, что первый проход уже был,
|
|
||||||
* иначе при печати второй стороны инструкция очистки может появиться снова.
|
|
||||||
*/
|
|
||||||
let hadFlip = $state(false);
|
|
||||||
let hadPickup = $state(false);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!store.job) return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
jobPhase === 'awaiting_flip' ||
|
|
||||||
jobFiles.some((f) => f.status === 'awaiting_flip')
|
|
||||||
) {
|
|
||||||
hadFlip = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
jobPhase === 'awaiting_pickup' ||
|
|
||||||
jobFiles.some((f) => f.status === 'awaiting_pickup')
|
|
||||||
) {
|
|
||||||
hadPickup = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstSideFinished = $derived(
|
|
||||||
hadFlip ||
|
|
||||||
hadPickup ||
|
|
||||||
jobPhase === 'finished' ||
|
|
||||||
jobPhase === 'cancelled' ||
|
|
||||||
jobFiles.some((f) => f.status === 'done')
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инструкция очистки лотка:
|
|
||||||
* - если сервер явно просит awaiting_clear_output И есть уже напечатанные файлы;
|
|
||||||
* - для двусторонней печати — ДО первого прохода,
|
|
||||||
* пока ещё не было flip/pickup/done И есть напечатанные файлы
|
|
||||||
* (т.е. файл не первый в очереди и лоток уже занят).
|
|
||||||
*/
|
|
||||||
const showClearOutput = $derived(
|
|
||||||
store.jobActive &&
|
|
||||||
(
|
|
||||||
jobPhase === 'waiting_start' ||
|
|
||||||
jobPhase === 'awaiting_clear_output' ||
|
|
||||||
jobFiles.some((f) => f.status === 'awaiting_clear_output')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFlip = $derived(
|
const showFlip = $derived(
|
||||||
store.jobActive &&
|
store.jobActive &&
|
||||||
hasFlip &&
|
(
|
||||||
|
jobPhase === 'awaiting_flip' ||
|
||||||
|
jobFiles.some((f) => norm(f.status) === 'awaiting_flip')
|
||||||
|
) &&
|
||||||
jobPhase !== 'finished' &&
|
jobPhase !== 'finished' &&
|
||||||
jobPhase !== 'cancelled'
|
jobPhase !== 'cancelled'
|
||||||
);
|
);
|
||||||
|
|
||||||
const showPickup = $derived(
|
const showPickup = $derived(
|
||||||
store.jobActive &&
|
store.jobActive &&
|
||||||
hasPickup &&
|
(
|
||||||
|
jobPhase === 'awaiting_pickup' ||
|
||||||
|
jobFiles.some((f) => norm(f.status) === 'awaiting_pickup')
|
||||||
|
) &&
|
||||||
!showFlip &&
|
!showFlip &&
|
||||||
jobPhase !== 'finished' &&
|
jobPhase !== 'finished' &&
|
||||||
jobPhase !== 'cancelled'
|
jobPhase !== 'cancelled'
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearActionAvailable = $derived(
|
const clearActionAvailable = $derived(showClearOutput);
|
||||||
showClearOutput &&
|
|
||||||
(jobPhase === 'awaiting_clear_output' || jobPhase === 'waiting_start')
|
|
||||||
);
|
|
||||||
|
|
||||||
const flipActionAvailable = $derived(
|
const flipActionAvailable = $derived(
|
||||||
showFlip && jobPhase === 'awaiting_flip'
|
showFlip && jobPhase === 'awaiting_flip'
|
||||||
@ -150,29 +105,65 @@
|
|||||||
showPickup && jobPhase === 'awaiting_pickup'
|
showPickup && jobPhase === 'awaiting_pickup'
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearButtonLabel = $derived(
|
const clearButtonLabel = $derived(
|
||||||
jobPhase === 'waiting_start'
|
showStart ? 'Начать печать' : 'Убрал листы — продолжать'
|
||||||
? 'Начать печать'
|
);
|
||||||
: 'Убрал листы — продолжать'
|
|
||||||
);
|
/**
|
||||||
|
* Автостарт.
|
||||||
|
*
|
||||||
|
* Если сервер вернул задание в waiting_start, а фронт по какой-то причине
|
||||||
|
* не стартовал его сам, стартуем здесь.
|
||||||
|
*
|
||||||
|
* Если тебе нужен ТОЛЬКО ручной старт, удали этот $effect целиком.
|
||||||
|
*/
|
||||||
|
let autoStartedJobId = '';
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const jobId = store.job?.job_id;
|
||||||
|
|
||||||
|
if (!store.jobActive) return;
|
||||||
|
if (!jobId) return;
|
||||||
|
if (jobPhase !== 'waiting_start') return;
|
||||||
|
if (autoStartedJobId === jobId) return;
|
||||||
|
|
||||||
|
autoStartedJobId = jobId;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (
|
||||||
|
store.job?.job_id === jobId &&
|
||||||
|
store.jobActive &&
|
||||||
|
norm(store.job?.phase) === 'waiting_start'
|
||||||
|
) {
|
||||||
|
store.advanceJob();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* В какой карточке файла показывать инструкцию.
|
* В какой карточке файла показывать инструкцию.
|
||||||
* Инструкция всегда живёт ВНУТРИ карточки соответствующего файла,
|
|
||||||
* а не отдельным блоком над списком.
|
|
||||||
*/
|
*/
|
||||||
const flipTarget = $derived(
|
const flipTarget = $derived(
|
||||||
showFlip ? (jobFiles.find((f) => f.status === 'awaiting_flip') ?? null) : null
|
showFlip
|
||||||
|
? (jobFiles.find((f) => norm(f.status) === 'awaiting_flip') ?? null)
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const pickupTarget = $derived(
|
const pickupTarget = $derived(
|
||||||
showPickup ? (jobFiles.find((f) => f.status === 'awaiting_pickup') ?? null) : null
|
showPickup
|
||||||
|
? (jobFiles.find((f) => norm(f.status) === 'awaiting_pickup') ?? null)
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearTarget = $derived.by(() => {
|
const clearTarget = $derived.by(() => {
|
||||||
if (!showClearOutput) return null;
|
if (!showClearOutput) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jobFiles.find((f) => f.status === 'awaiting_clear_output') ??
|
jobFiles.find((f) => norm(f.status) === 'awaiting_clear_output') ??
|
||||||
(jobPhase !== 'waiting_start' ? jobFiles.find((f) => f.status === 'printing') : undefined) ??
|
(jobPhase !== 'waiting_start'
|
||||||
jobFiles.find((f) => f.status === 'queued') ??
|
? jobFiles.find((f) => norm(f.status) === 'printing')
|
||||||
|
: undefined) ??
|
||||||
|
jobFiles.find((f) => norm(f.status) === 'queued') ??
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -200,12 +191,14 @@
|
|||||||
|
|
||||||
const step = (now) => {
|
const step = (now) => {
|
||||||
hold = Math.min(1, (now - t0) / HOLD_MS);
|
hold = Math.min(1, (now - t0) / HOLD_MS);
|
||||||
|
|
||||||
if (hold >= 1) {
|
if (hold >= 1) {
|
||||||
raf = 0;
|
raf = 0;
|
||||||
hold = 0;
|
hold = 0;
|
||||||
store.cancelJob();
|
store.cancelJob();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
raf = requestAnimationFrame(step);
|
raf = requestAnimationFrame(step);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -232,42 +225,44 @@
|
|||||||
|
|
||||||
<main class="list">
|
<main class="list">
|
||||||
{#if store.job}
|
{#if store.job}
|
||||||
{#each store.job.files as f, i (i + '-' + f.name)}
|
{#each jobFiles as f, i (i + '-' + f.name)}
|
||||||
{@const b = badge(f.name)}
|
{@const b = badge(f.name)}
|
||||||
|
{@const st = norm(f.status)}
|
||||||
{@const hint = hintFor(f)}
|
{@const hint = hintFor(f)}
|
||||||
{@const isActive = f.status === 'awaiting_flip'
|
{@const isActive =
|
||||||
|| f.status === 'awaiting_clear_output'
|
['awaiting_flip', 'awaiting_clear_output', 'awaiting_pickup', 'printing'].includes(st) ||
|
||||||
|| f.status === 'awaiting_pickup'
|
hint !== null}
|
||||||
|| f.status === 'printing'
|
|
||||||
|| hint !== null}
|
|
||||||
|
|
||||||
<section class="file-card" class:current={isActive}>
|
<section class="file-card" class:current={isActive}>
|
||||||
<div class="file-row">
|
<div class="file-row">
|
||||||
{#if b}<span class="type-badge">{b}</span>{/if}
|
{#if b}<span class="type-badge">{b}</span>{/if}
|
||||||
|
|
||||||
<span class="file-name" title={f.name}>{f.name}</span>
|
<span class="file-name" title={f.name}>{f.name}</span>
|
||||||
<span class="file-status st-{f.status}">
|
|
||||||
{STATUS_LABEL[f.status] ?? f.status}
|
<span class="file-status st-{st}">
|
||||||
|
{STATUS_LABEL[st] ?? f.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if hint === 'clear'}
|
{#if hint === 'clear'}
|
||||||
<ol class="pickup-hint clear-hint">
|
<ol class="pickup-hint clear-hint">
|
||||||
{#if jobPhase !== 'waiting_start'}
|
{#if !showStart}
|
||||||
<li>Уберите распечатанные листы из выходного лотка</li>
|
<li>Уберите распечатанные листы из выходного лотка</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if clearActionAvailable}
|
{#if clearActionAvailable}
|
||||||
<li>
|
<li>
|
||||||
Нажмите кнопку ниже
|
Нажмите кнопку ниже
|
||||||
{jobPhase === 'waiting_start' ? 'для начала печати' : 'для продолжения'}
|
{showStart ? 'для начала печати' : 'для продолжения'}
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>Дождитесь завершения первого прохода</li>
|
<li>Дождитесь завершения первого прохода</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ol>
|
</ol>
|
||||||
{:else if hint === 'flip'}
|
{:else if hint === 'flip'}
|
||||||
<ol class="pickup-hint flip-hint">
|
<ol class="pickup-hint flip-hint">
|
||||||
<li>Не переворачивая, положите листы в слот ручной подачи</li>
|
<li>Не переворачивая, положите листы в слот ручной подачи</li>
|
||||||
|
|
||||||
{#if flipActionAvailable}
|
{#if flipActionAvailable}
|
||||||
<li>Нажмите кнопку ниже для печати обратной стороны</li>
|
<li>Нажмите кнопку ниже для печати обратной стороны</li>
|
||||||
{:else}
|
{:else}
|
||||||
@ -277,6 +272,7 @@
|
|||||||
{:else if hint === 'pickup'}
|
{:else if hint === 'pickup'}
|
||||||
<ol class="pickup-hint">
|
<ol class="pickup-hint">
|
||||||
<li>Дождитесь завершения печати</li>
|
<li>Дождитесь завершения печати</li>
|
||||||
|
|
||||||
{#if pickupActionAvailable}
|
{#if pickupActionAvailable}
|
||||||
<li>Нажмите кнопку ниже для подтверждения</li>
|
<li>Нажмите кнопку ниже для подтверждения</li>
|
||||||
{:else}
|
{:else}
|
||||||
@ -308,16 +304,20 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if store.jobActive}
|
{#if store.jobActive}
|
||||||
{#if pickupActionAvailable}
|
{#if showStart}
|
||||||
<button class="next-btn" onclick={store.advanceJob}>
|
<button class="next-btn" onclick={() => store.advanceJob()}>
|
||||||
|
{clearButtonLabel}
|
||||||
|
</button>
|
||||||
|
{:else if pickupActionAvailable}
|
||||||
|
<button class="next-btn" onclick={() => store.advanceJob()}>
|
||||||
Печать закончена — далее
|
Печать закончена — далее
|
||||||
</button>
|
</button>
|
||||||
{:else if flipActionAvailable}
|
{:else if flipActionAvailable}
|
||||||
<button class="next-btn next-btn-flip" onclick={store.advanceJob}>
|
<button class="next-btn next-btn-flip" onclick={() => store.advanceJob()}>
|
||||||
Переложил — печатать обратную сторону
|
Переложил — печатать обратную сторону
|
||||||
</button>
|
</button>
|
||||||
{:else if clearActionAvailable}
|
{:else if clearActionAvailable}
|
||||||
<button class="next-btn next-btn-clear" onclick={store.advanceJob}>
|
<button class="next-btn next-btn-clear" onclick={() => store.advanceJob()}>
|
||||||
{clearButtonLabel}
|
{clearButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
@ -479,7 +479,6 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Подсказки живут ВНУТРИ карточки файла, текст — по левому краю */
|
|
||||||
.pickup-hint {
|
.pickup-hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
|
|||||||
Reference in New Issue
Block a user