diff --git a/README.md b/README.md index 54a2631..d992c54 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,58 @@ -# Svelte + Vite +# Структура проекта -This template should help get you started developing with Svelte in Vite. +. +├── app.css +├── App.svelte +├── assets +│ ├── hero.png +│ ├── svelte.svg +│ └── vite.svg +├── lib +│ ├── common +│ │ ├── PaymentPage.svelte +│ │ ├── services +│ │ ├── stores +│ │ ├── ui +│ │ └── utils +│ └── pages +│ ├── extra +│ ├── main +│ │ └── MainMenu.svelte +│ ├── photo-doc +│ ├── print +│ │ ├── preview +│ │ └── PrintMenu.svelte +│ └── scan +└── main.js -## Recommended IDE Setup +--- -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). +### 📂 Корень проекта +* **`App.svelte`**: Главный роутер. Управляет переключением между экранами (Главное меню → Печать/Фото/Скан). +* **`main.js`**: Точка входа, монтирует приложение в DOM. +* **`app.css`**: Глобальные стили и сброс CSS. +* **`assets/`**: Статические файлы (иконки, фоновые изображения, логотипы). -## Need an official Svelte framework? +--- -Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. +### 📂 `lib/modules/` (Бизнес-модули) +Каждая папка — это независимая услуга киоска. Содержит свои UI-компоненты и локальные сторы состояния. +* **`main/`**: Главное меню с 4 кнопками навигации. +* **`print/`**: Логика печати (загрузка PDF/фото, выбор страниц, настройки формата/цвета). +* **`photo-doc/`**: Фото на документы (работа с камерой, кроп, выбор размера). +* **`scan/`**: Сканирование документов (скан через камеру или загрузка файла). +* **`extra/`**: Дополнительные услуги (копирование, отправка на email и т.д.). -## Technical considerations +--- -**Why use this over SvelteKit?** +### 📂 `lib/common/` (Общая инфраструктура) +Ресурсы, которые переиспользуются несколькими модулями. -- It brings its own routing solution which might not be preferable for some users. -- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. +#### **UI-слой** +* **`ui/`**: Переиспользуемые визуальные компоненты (кнопки, инпуты, карточки, лоадеры). +* **`PaymentPage.svelte`**: Единый экран оплаты P2P-переводом. Используется всеми модулями после формирования заказа. -This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. - -Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. - -**Why include `.vscode/extensions.json`?** - -Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. - -**Why enable `checkJs` in the JS template?** - -It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. - -**Why is HMR not preserving my local component state?** - -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). - -If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. - -```js -// store.js -// An extremely simple external store -import { writable } from 'svelte/store' -export default writable(0) -``` +#### **Логика и данные** +* **`stores/`**: Глобальные хранилища состояния (навигация, настройки приложения, очередь уведомлений). +* **`services/`**: Интеграции с внешним миром (API принтера, работа с PDF.js, доступ к камере, отправка запросов на сервер). +* **`utils/`**: Чистые вспомогательные функции (расчет стоимости, форматирование текста, детекция типов файлов, плюрализация). diff --git a/jsconfig.json b/jsconfig.json index c7a0b10..bc44f3b 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,31 +3,18 @@ "moduleResolution": "bundler", "target": "ESNext", "module": "ESNext", - /** - * svelte-preprocess cannot figure out whether you have - * a value or a type, so tell TypeScript to enforce using - * `import type` instead of `import` for Types. - */ "verbatimModuleSyntax": true, "isolatedModules": true, "resolveJsonModule": true, - /** - * To have warnings / errors of the Svelte compiler at the - * correct position, enable source maps by default. - */ "sourceMap": true, "esModuleInterop": true, "types": ["vite/client"], "skipLibCheck": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable this if you'd like to use dynamic types. - */ - "checkJs": true + "checkJs": true, + "baseUrl": ".", + "paths": { + "$lib/*": ["src/lib/*"] + } }, - /** - * Use global.d.ts instead of compilerOptions.types - * to avoid limiting type declarations. - */ "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] } diff --git a/package-lock.json b/package-lock.json index d510ed1..d6504fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3115,9 +3115,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.422", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", - "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "version": "1.5.423", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.423.tgz", + "integrity": "sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==", "dev": true, "license": "ISC" }, diff --git a/src/App.svelte b/src/App.svelte index dc24aea..4731018 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,66 +1,57 @@
- {#if page === 'main'} - - {/if} - {#if page === 'print'} - - {/if} + {#if page === 'main'} + + {:else if page === 'print'} + + {/if}
diff --git a/src/lib/PrintMenu.svelte b/src/lib/PrintMenu.svelte deleted file mode 100644 index d486821..0000000 --- a/src/lib/PrintMenu.svelte +++ /dev/null @@ -1,668 +0,0 @@ - - - - - -{#if showPayment} - showPayment = false} - onConfirmPayment={realSubmitPrint} - /> -{/if} - - - - -{#if !activeFile && !showPayment} -
-
- -

Печать

-
-
- {#if files.length === 0} -
-

Загрузите файлы для печати

- -
- {:else} - {#each files as file (file.id)} -
-
toggleExpanded(file)}> - {#if file.expanded}▼{:else}►{/if} - {file.file.name} - e.stopPropagation()}> - {#if file.previewUrl} - - {/if} - - -
- {#if file.expanded} -
- -
- -
- {#each formats as f} - - {/each} -
-
-
- -
- {#each colorModes as c} - - {/each} -
-
-
- - updateQuality(file, parseInt(e.target.value))} - class="quality-slider" - style="--fill: {qualityFillFor(file)}%" - /> -
- {#each qualities as q}{q.label}{/each} -
-
-
- {/if} -
- {/each} - - {/if} -
-
- {#if files.length > 0} -

- {files.length} {plFiles(files.length)} - {#if extraCopies > 0}, {extraCopies} {plCopies(extraCopies)}{/if} -

- {/if} - - -
- (f.selectedPages?.size || 0) < 25)}>10р/л от 1 - { const s = f.selectedPages?.size || 0; return s >= 25 && s < 1000; })}>9р/л от 25 - (f.selectedPages?.size || 0) >= 1000)}>4р/л от 1000 -
- -
- Итого - {totalPrice()}₽ -
- - - -
-
-{/if} - - - - -{#if activeFile} - -{/if} - - diff --git a/src/lib/common/services/pdf.service.js b/src/lib/common/services/pdf.service.js new file mode 100644 index 0000000..3a66272 --- /dev/null +++ b/src/lib/common/services/pdf.service.js @@ -0,0 +1,51 @@ +// lib/common/services/pdf.service.js + +let pdfjsLib = null; + +async function getPdfJs() { + if (!pdfjsLib) { + pdfjsLib = await import('https://mozilla.github.io/pdf.js/build/pdf.mjs'); + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.mjs'; + } + return pdfjsLib; +} + +/** + * Загрузка PDF документа + * @param {File} file + */ +export async function loadPdfDocument(file) { + try { + const lib = await getPdfJs(); + const arrayBuffer = await file.arrayBuffer(); + const loadingTask = lib.getDocument({ data: arrayBuffer }); + return await loadingTask.promise; + } catch (err) { + console.error('PDF load error:', err); + return null; + } +} + +/** + * Рендер страницы PDF в DataURL изображения + * @param {any} pdfDoc + * @param {number} pageNum + * @param {number} scale + */ +export async function renderPageToImage(pdfDoc, pageNum, scale = 1.0) { + const page = await pdfDoc.getPage(pageNum); + const viewport = page.getViewport({ scale }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const outputScale = window.devicePixelRatio || 1; + + canvas.width = Math.floor(viewport.width * outputScale); + canvas.height = Math.floor(viewport.height * outputScale); + canvas.style.width = `${Math.floor(viewport.width)}px`; + canvas.style.height = `${Math.floor(viewport.height)}px`; + + const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined; + + await page.render({ canvasContext: context, viewport, transform }).promise; + return canvas.toDataURL('image/jpeg', 0.85); +} diff --git a/src/lib/PaymentPage.svelte b/src/lib/common/ui/PaymentPage.svelte similarity index 100% rename from src/lib/PaymentPage.svelte rename to src/lib/common/ui/PaymentPage.svelte diff --git a/src/lib/common/utils/file.util.js b/src/lib/common/utils/file.util.js new file mode 100644 index 0000000..e749b4c --- /dev/null +++ b/src/lib/common/utils/file.util.js @@ -0,0 +1,25 @@ +// lib/common/utils/file.util.js + +/** + * Определение типа файла + * @param {File} file + * @returns {'image' | 'pdf' | 'other'} + */ +export function detectFileType(file) { + if (!file) return 'other'; + if (file.type.startsWith('image/')) return 'image'; + if (file.type === 'application/pdf') return 'pdf'; + return 'other'; +} + +/** + * Формирование текстовой метки выбранных страниц + * @param {import('$lib/pages/print/stores/print.store.svelte').FileEntry} entry + */ +export function pagesLabel(entry) { + if (!entry.totalPages || entry.totalPages === 0) return '...'; + if (!entry.selectedPages || entry.selectedPages.size === 0) return 'Нет'; + if (entry.selectedPages.size === entry.totalPages) return 'Все'; + if (entry.selectedPages.size === 1) return `стр. ${[...entry.selectedPages][0]}`; + return `${entry.selectedPages.size} из ${entry.totalPages}`; +} diff --git a/src/lib/common/utils/pricing.util.js b/src/lib/common/utils/pricing.util.js new file mode 100644 index 0000000..9bb6d33 --- /dev/null +++ b/src/lib/common/utils/pricing.util.js @@ -0,0 +1,37 @@ +// lib/common/utils/pricing.util.js + +export const QUALITIES = [ + { id: 'low', label: 'Эконом' }, + { id: 'medium', label: 'Стандарт' }, + { id: 'high', label: 'Максимум' } +]; + +export const COLOR_MODES = [ + { id: 'bw', label: 'Чб' }, + { id: 'color', label: 'Цвет' } +]; + +export const FORMATS = ['A4', 'A5']; + +/** + * Расчет цены за страницу в зависимости от объема + */ +export function getPricePerPage(pagesCount) { + if (pagesCount >= 1000) return 4; + if (pagesCount >= 25) return 9; + return 10; +} + +/** + * Плюрализация + */ +export function pluralize(n, forms) { + const mod10 = n % 10; + const mod100 = n % 100; + if (mod10 === 1 && mod100 !== 11) return forms[0]; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 > 14)) return forms[1]; + return forms[2]; +} + +export const plFiles = (n) => pluralize(n, ['файл', 'файла', 'файлов']); +export const plCopies = (n) => pluralize(n, ['копия', 'копии', 'копий']); diff --git a/src/lib/MainMenu.svelte b/src/lib/pages/main/MainMenu.svelte similarity index 100% rename from src/lib/MainMenu.svelte rename to src/lib/pages/main/MainMenu.svelte diff --git a/src/lib/pages/print/PreviewOverlay.svelte b/src/lib/pages/print/PreviewOverlay.svelte new file mode 100644 index 0000000..b837e77 --- /dev/null +++ b/src/lib/pages/print/PreviewOverlay.svelte @@ -0,0 +1,41 @@ + + + +{#if store.activeFile} + +{/if} + + diff --git a/src/lib/pages/print/PrintMenu.svelte b/src/lib/pages/print/PrintMenu.svelte new file mode 100644 index 0000000..270fef5 --- /dev/null +++ b/src/lib/pages/print/PrintMenu.svelte @@ -0,0 +1,89 @@ + + + +{#if store.showPayment} + +{/if} + +{#if !store.activeFile && !store.showPayment} +
+
+ +

Печать

+
+ +
+ {#if store.files.length === 0} + fileInput.click()} /> + {:else} + {#each store.files as file (file.id)} + store.openPreview(f)} + onPageSelect={(f) => (f.fileType === 'pdf' ? store.openGallery(f) : store.openPreview(f))} + pagesLabel={store.pagesLabel} + /> + {/each} + + {/if} +
+ + + + +
+{/if} + + + + diff --git a/src/lib/pages/print/PrintUI.svelte b/src/lib/pages/print/PrintUI.svelte new file mode 100644 index 0000000..d362973 --- /dev/null +++ b/src/lib/pages/print/PrintUI.svelte @@ -0,0 +1,184 @@ + + + +{#if as === 'button'} + + +{:else if as === 'icon-button'} + + +{:else if as === 'counter'} +
+ + {props.value} шт. + +
+ +{:else if as === 'toggle-group'} +
+ {#if props.label}{/if} +
+ {#each props.options as opt} + {@const val = props.valueKey ? opt[props.valueKey] : opt} + {@const lbl = props.labelKey ? opt[props.labelKey] : opt} + + {/each} +
+
+ +{:else if as === 'range-slider'} +
+ + props.onChange?.(parseInt(e.target.value))} + class="quality-slider" + style="--fill: {Math.round((props.value / (props.options.length - 1)) * 100)}%" + /> +
+ {#each props.options as q}{q.label}{/each} +
+
+ +{:else if as === 'file-card'} + {@const file = props.file} +
+
props.onToggle?.(file.id)}> + {file.expanded ? '▼' : '►'} + {file.file.name} + e.stopPropagation()}> + {#if file.previewUrl} + props.onPreview?.(file)} title="Предпросмотр">🔍 + {/if} + props.onRemove?.(file.id)} title="Удалить">✕ + +
+ {#if file.expanded} +
+ + props.onUpdate?.(file.id, { format: v })} /> + props.onUpdate?.(file.id, { colorMode: v })} /> + props.onUpdate?.(file.id, { qualityIndex: v })} /> +
+ {/if} +
+ +{:else if as === 'upload-zone'} +
+

Загрузите файлы для печати

+ Загрузить файл(ы) +
+ +{:else if as === 'footer'} +
+ {#if props.filesCount > 0} +

+ {props.filesCount} {props.plFiles(props.filesCount)} + {#if props.extraCopies > 0}, {props.extraCopies} {props.plCopies(props.extraCopies)}{/if} +

+ {/if} +
+ 10р/л от 1 + 9р/л от 25 + 4р/л от 1000 +
+
+ Итого + {props.totalPrice}₽ +
+ + {props.isPrinting ? 'Отправка...' : 'Далее'} + +
+{/if} + + diff --git a/src/lib/preview/PreviewPage.svelte b/src/lib/pages/print/file_preview/PreviewPage.svelte similarity index 100% rename from src/lib/preview/PreviewPage.svelte rename to src/lib/pages/print/file_preview/PreviewPage.svelte diff --git a/src/lib/preview/SelectionPage.svelte b/src/lib/pages/print/file_preview/SelectionPage.svelte similarity index 100% rename from src/lib/preview/SelectionPage.svelte rename to src/lib/pages/print/file_preview/SelectionPage.svelte diff --git a/src/lib/pages/print/store.svelte.js b/src/lib/pages/print/store.svelte.js new file mode 100644 index 0000000..ca5e646 --- /dev/null +++ b/src/lib/pages/print/store.svelte.js @@ -0,0 +1,243 @@ +// 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' | 'other', expanded: boolean, format: 'A4' | 'A5', colorMode: 'bw' | 'color', qualityIndex: number, copies: number, totalPages: number, selectedPages: Set, pdfDoc: any }} FileEntry + */ + +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 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); + newFiles.push({ + id: ++uid, + file, + previewUrl: URL.createObjectURL(file), + fileType: fType, + expanded: false, + format: 'A4', + colorMode: 'bw', + qualityIndex: 1, + copies: 1, + totalPages: fType === 'image' ? 1 : 0, + selectedPages: fType === 'image' ? 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); + formData.append( + 'settings', + JSON.stringify({ + filename: f.file.name, + pages: f.selectedPages ? Array.from(f.selectedPages).sort((a, b) => a - b).join(',') : 'all', + copies: f.copies, + colorMode: f.colorMode, + format: f.format + }) + ); + } + try { + const res = await fetch('/api/print', { method: 'POST', body: formData }); + const text = await res.text(); + alert(res.ok ? `✅ Успешно!\n${text}` : `❌ Ошибка (${res.status}):\n${text}`); + } catch { + alert('⚠️ Не удалось связаться с сервером печати.'); + } finally { + isPrinting = false; + showPayment = false; + } + } + + 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; }, + + addFiles, + removeFile, + toggleExpand, + updateFile, + openGallery, + openPreview, + closeOverlay: () => { activeFile = null; galleryThumbnails = []; previewPagesCache = {}; }, + openPayment: () => { if (files.length > 0 && !isPrinting) showPayment = true; }, + hidePayment: () => { showPayment = false; }, + submitPrint, + togglePageSelection, + selectAllPages, + deselectAllPages, + requestPreviewPages: (start, count) => { if (activeFile) preloadPreviewPages(activeFile, start, count); }, + pagesLabel: _pagesLabel + }; +} diff --git a/update_nvm.sh b/start.sh similarity index 92% rename from update_nvm.sh rename to start.sh index 848d957..3006a60 100755 --- a/update_nvm.sh +++ b/start.sh @@ -9,3 +9,5 @@ nvm install --lts # Обновляем npm до последней версии npm install -g npm@latest + +npm run dev -- --host diff --git a/vite.config.js b/vite.config.js index f65f1c4..abae67e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' import { VitePWA } from 'vite-plugin-pwa' +import { fileURLToPath, URL } from 'node:url' export default defineConfig({ plugins: [ @@ -24,6 +25,11 @@ export default defineConfig({ } }) ], + resolve: { + alias: { + $lib: fileURLToPath(new URL('./src/lib', import.meta.url)) + } + }, server: { proxy: { '/api': {