Files
app/src/lib/pages/print/PrintProgress.svelte

212 lines
9.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- lib/pages/print/PrintProgress.svelte -->
<script>
const currentStatus = $derived.by(() => {
if (!store.job) return 'idle';
const f = store.job.files.find(f => f.status === 'printing');
if (f) return 'printing';
const q = store.job.files.find(f => f.status === 'queued');
return q ? 'queued' : store.job.phase;
});
let { store, onBack = () => {} } = $props();
const OFFICE_EXT = ['DOC', 'DOCX', 'ODT', 'XLS', 'XLSX', 'ODS', 'PPT', 'PPTX', 'ODP'];
const STATUS_LABEL = {
queued: 'Ожидает',
printing: 'Печатается…',
awaiting_flip: 'Переложите бумагу',
done: 'Готово',
error: 'Ошибка',
cancelled: 'Отменено'
};
function badge(name) {
const e = (name.split('.').pop() || '').toUpperCase();
return OFFICE_EXT.includes(e) ? e : null;
}
// Опрос статуса задания, пока оно активно
$effect(() => {
if (!store.jobActive) return;
const int = setInterval(() => store.refreshJob(), 1000);
return () => clearInterval(int);
});
// ── Отмена удержанием (защита от миссклика) ──
const HOLD_MS = 1500;
let hold = $state(0);
let raf = 0;
let t0 = 0;
function startHold(e) {
if (!store.jobActive) return;
e.preventDefault();
e.currentTarget.setPointerCapture?.(e.pointerId);
t0 = performance.now();
const step = (now) => {
hold = Math.min(1, (now - t0) / HOLD_MS);
if (hold >= 1) {
raf = 0;
hold = 0;
store.cancelJob();
return;
}
raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
}
function endHold() {
if (raf) cancelAnimationFrame(raf);
raf = 0;
hold = 0;
}
function finish() {
store.resetJob();
onBack();
}
</script>
<div class="page-container">
<header class="header">
<button class="back-btn" disabled={store.jobActive} onclick={finish}>←</button>
<h1>Печать</h1>
</header>
<main class="list">
{#if store.job}
{#each store.job.files as f, i (i + '-' + f.name)}
{@const b = badge(f.name)}
{@const isLastDone = f.status === 'done' && !store.job.files.slice(i + 1).some(f => f.status === 'printing' || f.status === 'queued')}
{@const hasNextQueued = store.job.files.slice(i + 1).some(f => f.status === 'queued')}
<section class="file-card" class:current={f.status === 'awaiting_flip' || f.status === 'printing' || isLastDone}>
<div class="file-row">
{#if b}<span class="type-badge">{b}</span>{/if}
<span class="file-name" title={f.name}>{f.name}</span>
<span class="file-status st-{f.status}">{STATUS_LABEL[f.status] ?? f.status}</span>
</div>
<!-- ── Инструкция для РУЧНОГО ДУПЛЕКСА (после 1-го прохода) ── -->
{#if f.status === 'awaiting_flip'}
<ol class="pickup-hint flip-hint">
<li>Не переворачивая, положите остальные листы в слот ручной подачи</li>
</ol>
{/if}
<!-- ── Инструкция для обычного «забери документ» ── -->
{#if isLastDone && hasNextQueued}
<ol class="pickup-hint">
<li>Возьмите готовый документ</li>
<li>Нажмите далее</li>
</ol>
{/if}
{#if f.error}
<p class="file-error">{f.error}</p>
{/if}
</section>
{/each}
{/if}
</main>
<footer class="actions">
<label class="auto-row">
<input
type="checkbox"
checked={store.autoContinue}
onchange={(e) => store.setAutoContinue(e.target.checked)}
/>
<span>Автоматически продолжать печать</span>
</label>
{#if store.jobActive}
{#if store.job?.phase === 'awaiting_flip'}
<!-- Ручной дуплекс: пользователь переложил бумагу -->
<button class="next-btn next-btn-flip" onclick={store.advanceJob}>
Готово — печатать обратную сторону
</button>
{:else if store.job?.phase === 'awaiting_pickup' && store.job.files.some(f => f.status === 'queued')}
<!-- Обычное: забрал документ → следующий (только если есть следующий) -->
<button class="next-btn" onclick={store.advanceJob}>Далее</button>
{:else}
<!-- Кнопка видна, но серая и disabled пока сервер не допечатал -->
<button class="next-btn next-btn-disabled" disabled>
{currentStatus === 'printing' ? 'Печатается…' : 'Ожидание печати…'}
</button>
{/if}
<!-- Кнопка отмены (оригинальный код) -->
<button
class="cancel-btn"
onpointerdown={startHold}
onpointerup={endHold}
onpointercancel={endHold}
onpointerleave={endHold}
oncontextmenu={(e) => e.preventDefault()}
>
<span class="cancel-fill" style:width="{hold * 100}%"></span>
<span class="cancel-label">Отмена</span>
</button>
<p class="hold-hint">Удерживайте для отмены печати</p>
{:else}
<button class="home-btn" onclick={finish}>На главную</button>
{/if}
</footer>
</div>
<style>
.st-awaiting_flip { color: #f59e0b; }
.flip-hint {
color: #f59e0b;
}
.next-btn-flip {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
box-shadow: 0 8px 24px -6px rgba(245, 158, 11, 0.4);
}
.page-container { width: 100%; min-height: 100dvh; display: flex; flex-direction: column; background: radial-gradient(120% 60% at 50% -10%, rgba(99,102,241,0.18) 0%, transparent 60%); font-family: system-ui, -apple-system, sans-serif; color: #f5f7fa; }
.header { padding: 24px 20px 8px; display: flex; align-items: center; position: relative; }
.back-btn { background: none; border: none; color: #6366f1; font-size: 15px; font-weight: 600; cursor: pointer; padding: 8px 4px; margin-right: auto; min-height: 44px; }
.back-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.header h1 { margin: 0; font-size: 28px; font-weight: 700; position: absolute; left: 50%; transform: translateX(-50%); white-space: nowrap; }
.list { flex: 1; padding: 24px 20px 16px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.file-card { border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); padding: 14px; display: flex; flex-direction: column; gap: 8px; transition: border-color 0.2s ease, background 0.2s ease; }
.file-card.current { border-color: rgba(99,102,241,0.45); background: rgba(99,102,241,0.07); }
.file-row { display: flex; align-items: center; gap: 12px; min-width: 0; }
.file-name { flex: 1; min-width: 0; font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-status { flex-shrink: 0; font-size: 14px; font-weight: 700; }
.st-done { color: #22c55e; }
.st-queued { color: #8b93a1; }
.st-printing { color: #a5b4fc; }
.st-error { color: #ef4444; }
.st-cancelled { color: #6b7280; }
.type-badge { flex-shrink: 0; padding: 3px 7px; border-radius: 6px; background: rgba(99,102,241,0.2); border: 1px solid rgba(99,102,241,0.35); color: #a5b4fc; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; line-height: 1.2; user-select: none; }
.pickup-hint { margin: 0; padding-left: 22px; color: #eab308; font-size: 14px; line-height: 1.6; font-weight: 600; }
.file-error { margin: 0; color: #fca5a5; font-size: 12px; line-height: 1.4; }
.actions { padding: 16px 20px 32px; display: flex; flex-direction: column; gap: 12px; }
.auto-row { display: flex; align-items: center; justify-content: center; gap: 10px; color: #a9b0c0; font-size: 14px; font-weight: 600; user-select: none; cursor: pointer; }
.auto-row input { width: 18px; height: 18px; accent-color: #6366f1; cursor: pointer; }
.next-btn { width: 100%; padding: 16px 24px; border-radius: 16px; font-size: 16px; font-weight: 700; cursor: pointer; border: none; color: #fff; background: linear-gradient(135deg, #5b8cff 0%, #38c6ff 100%); box-shadow: 0 8px 24px -6px rgba(72,150,255,0.4); }
.next-btn-disabled {
opacity: 0.5;
cursor: wait;
pointer-events: none;
background: linear-gradient(135deg, #475569 0%, #64748b 100%);
box-shadow: none;
}
.cancel-btn { position: relative; overflow: hidden; width: 100%; padding: 16px 24px; border-radius: 16px; border: 1px solid rgba(239,68,68,0.35); background: rgba(239,68,68,0.08); color: #fca5a5; font-size: 16px; font-weight: 700; cursor: pointer; touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; }
.cancel-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: rgba(239,68,68,0.35); pointer-events: none; }
.cancel-label { position: relative; }
.hold-hint { margin: -4px 0 0; text-align: center; color: rgba(139,147,161,0.6); font-size: 11px; }
.home-btn { width: 100%; padding: 16px 24px; border-radius: 16px; font-size: 16px; font-weight: 700; cursor: pointer; border: none; color: #fff; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); box-shadow: 0 8px 24px -6px rgba(34,197,94,0.4); }
</style>