v1.1.2 dualside work, have bugs/need work

This commit is contained in:
2026-09-19 23:11:26 +03:00
parent f059997a56
commit 41a775ea97
2 changed files with 60 additions and 173 deletions

View File

@ -205,5 +205,5 @@
<div class="version"> <div class="version">
<span>v{backendVersion}</span> <span>v{backendVersion}</span>
<span>v1.1.0</span> <span>v1.1.2</span>
</div> </div>

View File

@ -1,4 +1,3 @@
<!-- lib/pages/print/PrintProgress.svelte -->
<script> <script>
let { store, onBack = () => {} } = $props(); let { store, onBack = () => {} } = $props();
@ -31,25 +30,14 @@
const currentStatus = $derived.by(() => { const currentStatus = $derived.by(() => {
if (!store.job) return 'idle'; if (!store.job) return 'idle';
if (jobFiles.some((f) => norm(f.status) === 'printing')) return 'printing';
if (jobFiles.some((f) => norm(f.status) === 'printing')) { if (jobFiles.some((f) => norm(f.status) === 'queued')) return 'queued';
return 'printing';
}
if (jobFiles.some((f) => norm(f.status) === 'queued')) {
return 'queued';
}
return jobPhase; 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);
}); });
@ -57,116 +45,68 @@
jobFiles.some((f) => ['queued', 'printing'].includes(norm(f.status))) jobFiles.some((f) => ['queued', 'printing'].includes(norm(f.status)))
); );
/** // --- ЛОГИКА СОСТОЯНИЙ ---
* Явные состояния, при которых нужны пользовательские кнопки.
* Здесь больше не завязываемся на hasPrintedFiles / firstSideFinished. const showClearOutput = $derived(
*/ store.jobActive &&
(jobPhase === 'awaiting_clear_output' || jobFiles.some(f => norm(f.status) === 'awaiting_clear_output'))
);
const showStart = $derived( const showStart = $derived(
store.jobActive && jobPhase === 'waiting_start' store.jobActive && jobPhase === 'waiting_start'
); );
const showClearOutput = $derived(
store.jobActive &&
(
showStart ||
jobPhase === 'awaiting_clear_output' ||
jobFiles.some((f) => norm(f.status) === 'awaiting_clear_output')
)
);
const showFlip = $derived( const showFlip = $derived(
store.jobActive && store.jobActive &&
( (jobPhase === 'awaiting_flip' || jobFiles.some((f) => norm(f.status) === 'awaiting_flip')) &&
jobPhase === 'awaiting_flip' || jobPhase !== 'finished' && jobPhase !== 'cancelled'
jobFiles.some((f) => norm(f.status) === 'awaiting_flip')
) &&
jobPhase !== 'finished' &&
jobPhase !== 'cancelled'
); );
const showPickup = $derived( const showPickup = $derived(
store.jobActive && store.jobActive &&
( (jobPhase === 'awaiting_pickup' || jobFiles.some((f) => norm(f.status) === 'awaiting_pickup')) &&
jobPhase === 'awaiting_pickup' || !showFlip && !showClearOutput &&
jobFiles.some((f) => norm(f.status) === 'awaiting_pickup') jobPhase !== 'finished' && jobPhase !== 'cancelled'
) &&
!showFlip &&
jobPhase !== 'finished' &&
jobPhase !== 'cancelled'
); );
const clearActionAvailable = $derived(showClearOutput); const clearActionAvailable = $derived(showClearOutput);
const flipActionAvailable = $derived(showFlip && jobPhase === 'awaiting_flip');
const pickupActionAvailable = $derived(showPickup && jobPhase === 'awaiting_pickup');
const flipActionAvailable = $derived( const clearButtonLabel = $derived('Лоток пуст — продолжить');
showFlip && jobPhase === 'awaiting_flip'
);
const pickupActionAvailable = $derived( // Автостарт
showPickup && jobPhase === 'awaiting_pickup'
);
const clearButtonLabel = $derived(
showStart ? 'Начать печать' : 'Убрал листы — продолжать'
);
/**
* Автостарт.
*
* Если сервер вернул задание в waiting_start, а фронт по какой-то причине
* не стартовал его сам, стартуем здесь.
*
* Если тебе нужен ТОЛЬКО ручной старт, удали этот $effect целиком.
*/
let autoStartedJobId = ''; let autoStartedJobId = '';
$effect(() => { $effect(() => {
const jobId = store.job?.job_id; const jobId = store.job?.job_id;
if (!store.jobActive || !jobId) return;
if (!store.jobActive) return;
if (!jobId) return;
if (jobPhase !== 'waiting_start') return;
if (autoStartedJobId === jobId) return; if (autoStartedJobId === jobId) return;
if (!store.autoContinue) return;
// НЕ стартуем автоматически, если нужно убрать листы (требуется физическое действие)
if (showClearOutput) return;
// Автостарт для WaitingStart (начало задания)
if (jobPhase === 'waiting_start') {
autoStartedJobId = jobId; autoStartedJobId = jobId;
setTimeout(() => { setTimeout(() => {
if ( if (store.job?.job_id === jobId && norm(store.job?.phase) === 'waiting_start') {
store.job?.job_id === jobId &&
store.jobActive &&
norm(store.job?.phase) === 'waiting_start'
) {
store.advanceJob(); store.advanceJob();
} }
}, 50); }, 50);
}
}); });
/** // Целевые файлы для подсказок
* В какой карточке файла показывать инструкцию.
*/
const flipTarget = $derived( const flipTarget = $derived(
showFlip showFlip ? (jobFiles.find((f) => norm(f.status) === 'awaiting_flip') ?? null) : null
? (jobFiles.find((f) => norm(f.status) === 'awaiting_flip') ?? null)
: null
); );
const pickupTarget = $derived( const pickupTarget = $derived(
showPickup showPickup ? (jobFiles.find((f) => norm(f.status) === 'awaiting_pickup') ?? null) : null
? (jobFiles.find((f) => norm(f.status) === 'awaiting_pickup') ?? null)
: null
); );
const clearTarget = $derived(
const clearTarget = $derived.by(() => { showClearOutput ? (jobFiles.find((f) => norm(f.status) === 'awaiting_clear_output') ?? null) : null
if (!showClearOutput) return null;
return (
jobFiles.find((f) => norm(f.status) === 'awaiting_clear_output') ??
(jobPhase !== 'waiting_start'
? jobFiles.find((f) => norm(f.status) === 'printing')
: undefined) ??
jobFiles.find((f) => norm(f.status) === 'queued') ??
null
); );
});
/** @returns {'clear' | 'flip' | 'pickup' | null} */ /** @returns {'clear' | 'flip' | 'pickup' | null} */
function hintFor(f) { function hintFor(f) {
@ -176,6 +116,7 @@
return null; return null;
} }
// --- УДЕРЖАНИЕ ДЛЯ ОТМЕНЫ ---
const HOLD_MS = 1500; const HOLD_MS = 1500;
let hold = $state(0); let hold = $state(0);
let raf = 0; let raf = 0;
@ -183,38 +124,19 @@
function startHold(e) { function startHold(e) {
if (!store.jobActive || !canCancel) return; if (!store.jobActive || !canCancel) return;
e.preventDefault(); e.preventDefault();
e.currentTarget.setPointerCapture?.(e.pointerId); e.currentTarget.setPointerCapture?.(e.pointerId);
t0 = performance.now(); t0 = performance.now();
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) { raf = 0; hold = 0; store.cancelJob(); return; }
if (hold >= 1) {
raf = 0;
hold = 0;
store.cancelJob();
return;
}
raf = requestAnimationFrame(step); raf = requestAnimationFrame(step);
}; };
raf = requestAnimationFrame(step); raf = requestAnimationFrame(step);
} }
function endHold() { function endHold() { if (raf) cancelAnimationFrame(raf); raf = 0; hold = 0; }
if (raf) cancelAnimationFrame(raf); function finish() { store.resetJob(); onBack(); }
raf = 0;
hold = 0;
}
function finish() {
store.resetJob();
onBack();
}
</script> </script>
<div class="page-container"> <div class="page-container">
@ -236,9 +158,7 @@
<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-{st}"> <span class="file-status st-{st}">
{STATUS_LABEL[st] ?? f.status} {STATUS_LABEL[st] ?? f.status}
</span> </span>
@ -246,23 +166,12 @@
{#if hint === 'clear'} {#if hint === 'clear'}
<ol class="pickup-hint clear-hint"> <ol class="pickup-hint clear-hint">
{#if !showStart}
<li>Уберите распечатанные листы из выходного лотка</li> <li>Уберите распечатанные листы из выходного лотка</li>
{/if} <li>Нажмите кнопку ниже для начала печати этого файла</li>
{#if clearActionAvailable}
<li>
Нажмите кнопку ниже
{showStart ? 'для начала печати' : 'для продолжения'}
</li>
{:else}
<li>Дождитесь завершения первого прохода</li>
{/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}
@ -272,7 +181,6 @@
{: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}
@ -304,10 +212,15 @@
{/if} {/if}
{#if store.jobActive} {#if store.jobActive}
{#if showStart} {#if clearActionAvailable}
<button class="next-btn" onclick={() => store.advanceJob()}> <button class="next-btn next-btn-clear" onclick={() => store.advanceJob()}>
{clearButtonLabel} {clearButtonLabel}
</button> </button>
{:else if showStart}
<!-- Показываем только если автостарт выключен, иначе уедет само -->
<button class="next-btn" onclick={() => store.advanceJob()}>
Начать печать
</button>
{:else if pickupActionAvailable} {:else if pickupActionAvailable}
<button class="next-btn" onclick={() => store.advanceJob()}> <button class="next-btn" onclick={() => store.advanceJob()}>
Печать закончена — далее Печать закончена — далее
@ -316,10 +229,6 @@
<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}
<button class="next-btn next-btn-clear" onclick={() => store.advanceJob()}>
{clearButtonLabel}
</button>
{:else} {:else}
<button class="next-btn next-btn-disabled" disabled> <button class="next-btn next-btn-disabled" disabled>
{currentStatus === 'printing' ? 'Отправка...' : 'Ожидание...'} {currentStatus === 'printing' ? 'Отправка...' : 'Ожидание...'}
@ -399,10 +308,7 @@
min-height: 44px; min-height: 44px;
} }
.back-btn:disabled { .back-btn:disabled { opacity: 0.3; cursor: not-allowed; }
opacity: 0.3;
cursor: not-allowed;
}
.header h1 { .header h1 {
margin: 0; margin: 0;
@ -459,11 +365,7 @@
white-space: nowrap; white-space: nowrap;
} }
.file-status { .file-status { flex-shrink: 0; font-size: 14px; font-weight: 700; }
flex-shrink: 0;
font-size: 14px;
font-weight: 700;
}
.type-badge { .type-badge {
flex-shrink: 0; flex-shrink: 0;
@ -489,9 +391,7 @@
text-align: left; text-align: left;
} }
.pickup-hint li { .pickup-hint li { text-align: left; }
text-align: left;
}
.file-error { .file-error {
margin: 0; margin: 0;
@ -520,12 +420,7 @@
cursor: pointer; cursor: pointer;
} }
.auto-row input { .auto-row input { width: 18px; height: 18px; accent-color: #6366f1; cursor: pointer; }
width: 18px;
height: 18px;
accent-color: #6366f1;
cursor: pointer;
}
.next-btn { .next-btn {
width: 100%; width: 100%;
@ -577,25 +472,17 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.cancel-btn:disabled { .cancel-btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.cancel-fill { .cancel-fill {
position: absolute; position: absolute;
left: 0; left: 0; top: 0; bottom: 0;
top: 0;
bottom: 0;
width: 0%; width: 0%;
background: rgba(239,68,68,0.35); background: rgba(239,68,68,0.35);
pointer-events: none; pointer-events: none;
} }
.cancel-label { .cancel-label { position: relative; }
position: relative;
}
.hold-hint { .hold-hint {
margin: -4px 0 0; margin: -4px 0 0;