Files
web-app/src/lib/pages/print/store.svelte.js
2026-09-18 21:30:02 +03:00

334 lines
12 KiB
JavaScript
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/store.svelte.js
import { onDestroy, tick } from 'svelte';
import { getPricePerPage } from '$lib/common/utils/pricing.util.js';
import { detectFileType, pagesLabel as _pagesLabel } from '$lib/common/utils/file.util.js';
import { loadPdfDocument, renderPageToImage } from '$lib/common/services/pdf.service.js';
/**
* @typedef {{
* id: number, file: File, previewUrl: string | null,
* fileType: 'image' | 'pdf' | 'office' | 'other',
* expanded: boolean, format: string, colorMode: string,
* qualityIndex: number, copies: number, sides: string,
* dpi: string, totalPages: number,
* selectedPages: Set<number>, pdfDoc: any
* }} FileEntry
* @typedef {{ name: string, status: 'queued'|'printing'|'sent'|'awaiting_clear_output'|'awaiting_flip'|'awaiting_pickup'|'done'|'error'|'cancelled', error: (string|null) }} JobFileView
* @typedef {{ job_id: string, phase: 'waiting_start'|'printing'|'awaiting_clear_output'|'awaiting_flip'|'awaiting_pickup'|'finished'|'cancelled', files: JobFileView[] }} JobView
*/
export function createPrintStore() {
let files = $state(/** @type {FileEntry[]} */ ([]));
let activeFile = $state(/** @type {FileEntry | null} */ (null));
let viewMode = $state(/** @type {'gallery' | 'preview'} */ ('gallery'));
let showPayment = $state(false);
let isPrinting = $state(false);
let galleryThumbnails = $state([]);
let galleryLoading = $state(false);
let previewPagesCache = $state({});
let isPreviewRendering = $state(false);
let job = $state(/** @type {JobView | null} */ (null));
let autoContinue = $state(false);
let advancing = false;
const jobActive = $derived(!!job && job.phase !== 'finished' && job.phase !== 'cancelled');
let uid = 0;
const totalPrice = $derived(
files.reduce((sum, f) => {
const pages = f.selectedPages?.size || 0;
return sum + pages * f.copies * getPricePerPage(pages);
}, 0)
);
const extraCopies = $derived(files.reduce((sum, f) => sum + (f.copies - 1), 0));
function updateFile(id, changes) {
files = files.map((f) => (f.id === id ? { ...f, ...changes } : f));
if (activeFile?.id === id) {
activeFile = files.find((f) => f.id === id);
}
}
async function ensurePdfLoaded(entry) {
if (entry.pdfDoc) return entry.pdfDoc;
const pdf = await loadPdfDocument(entry.file);
if (pdf) {
const numPages = pdf.numPages;
updateFile(entry.id, {
pdfDoc: pdf,
totalPages: numPages,
selectedPages: new Set(Array.from({ length: numPages }, (_, i) => i + 1))
});
} else {
updateFile(entry.id, { totalPages: 0 });
}
return pdf;
}
async function addFiles(fileList) {
if (!fileList || fileList.length === 0) return;
const newFiles = [];
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
const fType = detectFileType(file);
const isOffice = fType === 'office';
newFiles.push({
id: ++uid,
file,
previewUrl: isOffice ? null : URL.createObjectURL(file),
fileType: fType,
expanded: false,
format: 'Авто',
colorMode: 'bw',
qualityIndex: 1,
copies: 1,
sides: 'Односторонняя',
dpi: 'Авто',
totalPages: (fType === 'image' || isOffice) ? 1 : 0,
selectedPages: (fType === 'image' || isOffice) ? new Set([1]) : new Set(),
pdfDoc: null
});
}
files = [...files, ...newFiles];
for (const entry of newFiles) {
if (entry.fileType === 'pdf') ensurePdfLoaded(entry);
}
}
function removeFile(id) {
const f = files.find((x) => x.id === id);
if (f?.previewUrl) URL.revokeObjectURL(f.previewUrl);
files = files.filter((x) => x.id !== id);
}
function toggleExpand(id) {
updateFile(id, { expanded: !files.find((f) => f.id === id)?.expanded });
}
async function openGallery(entry) {
activeFile = entry;
viewMode = 'gallery';
galleryThumbnails = [];
galleryLoading = true;
previewPagesCache = {};
await tick();
const pdf = await ensurePdfLoaded(entry);
if (!pdf || entry.totalPages === 0) {
galleryLoading = false;
return;
}
const thumbs = [];
for (let i = 1; i <= entry.totalPages; i++) {
try {
thumbs.push(await renderPageToImage(pdf, i, 0.4));
} catch {
thumbs.push('');
}
}
galleryThumbnails = [...thumbs];
galleryLoading = false;
}
async function openPreview(entry) {
activeFile = entry;
viewMode = 'preview';
previewPagesCache = {};
galleryThumbnails = [];
await tick();
if (entry.fileType === 'pdf') {
const pdf = await ensurePdfLoaded(entry);
if (pdf && entry.totalPages > 0) {
preloadPreviewPages(entry, 1, 5);
}
}
}
async function preloadPreviewPages(entry, startPage, count) {
const currentFile = files.find((f) => f.id === entry.id);
const pdf = currentFile?.pdfDoc || entry.pdfDoc;
if (!pdf || isPreviewRendering) return;
isPreviewRendering = true;
const endPage = Math.min(startPage + count - 1, entry.totalPages);
const newCache = {};
for (let i = startPage; i <= endPage; i++) {
if (previewPagesCache[i]) continue;
try {
newCache[i] = await renderPageToImage(pdf, i, 1.5);
} catch (e) {
console.error(`Failed to render preview page ${i}`, e);
}
}
if (Object.keys(newCache).length > 0) {
previewPagesCache = { ...previewPagesCache, ...newCache };
}
isPreviewRendering = false;
}
function togglePageSelection(pageNum) {
if (!activeFile?.selectedPages) return;
const newSet = new Set(activeFile.selectedPages);
if (newSet.has(pageNum)) {
if (newSet.size > 1) newSet.delete(pageNum);
} else {
newSet.add(pageNum);
}
updateFile(activeFile.id, { selectedPages: newSet });
}
function selectAllPages() {
if (!activeFile?.totalPages) return;
updateFile(activeFile.id, {
selectedPages: new Set(Array.from({ length: activeFile.totalPages }, (_, i) => i + 1))
});
}
function deselectAllPages() {
if (!activeFile) return;
updateFile(activeFile.id, { selectedPages: new Set([1]) });
}
async function submitPrint() {
if (files.length === 0 || isPrinting) return;
isPrinting = true;
const formData = new FormData();
for (const f of files) {
formData.append('files', f.file, f.file.name);
const pagesValue =
f.fileType === 'office'
? 'all'
: f.selectedPages && f.selectedPages.size > 0
? Array.from(f.selectedPages).sort((a, b) => a - b).join(',')
: 'all';
formData.append(
'settings',
JSON.stringify({
filename: f.file.name,
pages: pagesValue,
copies: f.copies,
colorMode: f.colorMode,
format: f.fileType === 'office' ? 'auto' : f.format,
sides: f.sides ?? 'Односторонняя',
quality: f.qualityIndex,
dpi: f.dpi ?? 'Авто'
})
);
}
try {
const res = await fetch('/api/print', { method: 'POST', body: formData });
const text = await res.text();
if (!res.ok) {
alert(`❌ Ошибка (${res.status}):\n${text}`);
return;
}
let data;
try {
data = JSON.parse(text);
} catch {
alert('⚠️ Некорректный ответ сервера печати.');
return;
}
job = data;
showPayment = false;
await advanceJob();
} catch {
alert('⚠️ Не удалось связаться с сервером печати.');
} finally {
isPrinting = false;
}
}
async function advanceJob() {
if (!job || advancing) return;
advancing = true;
try {
const res = await fetch(`/api/print/${job.job_id}/advance`, { method: 'POST' });
if (res.ok) job = await res.json();
} catch (e) {
console.error('advance error:', e);
} finally {
advancing = false;
}
}
async function refreshJob() {
if (!job) return;
try {
const res = await fetch(`/api/print/${job.job_id}`);
if (!res.ok) return;
job = await res.json();
if (job.phase === 'awaiting_pickup' && autoContinue) {
advanceJob();
}
} catch (e) {
console.error('refresh error:', e);
}
}
async function cancelJob() {
if (!job) return;
try {
const res = await fetch(`/api/print/${job.job_id}/cancel`, { method: 'POST' });
if (res.ok) job = await res.json();
} catch (e) {
console.error('cancel error:', e);
}
}
function resetJob() {
job = null;
autoContinue = false;
files.forEach((f) => f.previewUrl && URL.revokeObjectURL(f.previewUrl));
files = [];
activeFile = null;
galleryThumbnails = [];
previewPagesCache = {};
showPayment = false;
}
function setAutoContinue(v) {
autoContinue = !!v;
}
onDestroy(() => {
files.forEach((f) => f.previewUrl && URL.revokeObjectURL(f.previewUrl));
});
return {
get files() { return files; },
get activeFile() { return activeFile; },
get viewMode() { return viewMode; },
get showPayment() { return showPayment; },
get isPrinting() { return isPrinting; },
get totalPrice() { return totalPrice; },
get extraCopies() { return extraCopies; },
get galleryThumbnails() { return galleryThumbnails; },
get galleryLoading() { return galleryLoading; },
get previewPagesCache() { return previewPagesCache; },
get isPreviewRendering() { return isPreviewRendering; },
get job() { return job; },
get jobActive() { return jobActive; },
get autoContinue() { return autoContinue; },
addFiles,
removeFile,
toggleExpand,
updateFile,
openGallery,
openPreview,
closeOverlay: () => { activeFile = null; galleryThumbnails = []; previewPagesCache = {}; },
openPayment: () => { if (files.length > 0 && !isPrinting) showPayment = true; },
hidePayment: () => { showPayment = false; },
submitPrint,
advanceJob,
refreshJob,
cancelJob,
resetJob,
setAutoContinue,
togglePageSelection,
selectAllPages,
deselectAllPages,
requestPreviewPages: (start, count) => { if (activeFile) preloadPreviewPages(activeFile, start, count); },
pagesLabel: _pagesLabel
};
}