From 9b1d24def2839a856f452419604f1795e52c156d Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Wed, 4 Feb 2026 23:27:07 +0500 Subject: [PATCH] fix: resolve image download errors, update export and image services --- package-lock.json | 4 +- package.json | 1 + src/components/image/ImageCropper.svelte | 3 - src/components/image/ImageUpload.svelte | 4 +- src/components/layout/AppContainer.svelte | 2 +- src/components/layout/AppContent.svelte | 2 +- src/components/layout/Sidebar.svelte | 2 +- src/components/panel/PanelPreview.svelte | 53 +++++- src/components/panel/PanelsList.svelte | 7 +- src/lib/services/exportService.ts | 100 ---------- src/lib/services/imageService.ts | 214 ---------------------- src/routes/+page.svelte | 7 +- src/services/exportService.ts | 142 +++++++++----- src/services/imageService.ts | 87 +++++++++ 14 files changed, 251 insertions(+), 377 deletions(-) delete mode 100644 src/lib/services/exportService.ts delete mode 100644 src/lib/services/imageService.ts diff --git a/package-lock.json b/package-lock.json index e970436..23c7682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/file-saver": "^2.0.7", "@types/node": "^25.1.0", + "konva": "^10.2.0", "svelte": "^5.48.2", "svelte-check": "^4.3.5", "typescript": "^5.9.3", @@ -1413,8 +1414,7 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lie": { "version": "3.3.0", diff --git a/package.json b/package.json index 6b7c553..eedee4e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/file-saver": "^2.0.7", "@types/node": "^25.1.0", + "konva": "^10.2.0", "svelte": "^5.48.2", "svelte-check": "^4.3.5", "typescript": "^5.9.3", diff --git a/src/components/image/ImageCropper.svelte b/src/components/image/ImageCropper.svelte index 63971b0..9a7d13b 100644 --- a/src/components/image/ImageCropper.svelte +++ b/src/components/image/ImageCropper.svelte @@ -1,6 +1,5 @@ { + /** + * Экспортирует панель в изображение и сохраняет её + */ + async exportPanel(panel: Panel, konvaStage: Stage, filename?: string): Promise { try { - setLoading(true); - const result = await exportPanelService.exportPanel(panel); - - if (!result.success) { - this.errorMessage = result.error || "Ошибка экспорта панели"; + if (!konvaStage) { + throw new ImageError("Konva Stage не передан для экспорта"); } + + // Используем toBlob метод Konva Stage напрямую + const blob = await this.exportKonvaStageToBlob(konvaStage); + + if (!blob) { + throw new ImageError("Не удалось создать изображение"); + } + + const defaultFilename = `twitch-panel-${panel.id}.png`; + saveAs(blob, filename || defaultFilename); + + return { + success: true, + }; } catch (error) { - this.errorMessage = error instanceof Error ? error.message : "Ошибка экспорта панели"; - } finally { - setLoading(false); + logError(error, "Panel export failed"); + return { + success: false, + error: handleError(error, "Ошибка экспорта панели"), + }; } } + /** + * Экспортирует Konva Stage в Blob используя встроенный метод toBlob + */ + private async exportKonvaStageToBlob(konvaStage: Stage): Promise { + if (!konvaStage || typeof konvaStage.toBlob !== "function") { + throw new ImageError("Konva Stage не найден или не поддерживает toBlob"); + } + + return new Promise((resolve, reject) => { + konvaStage.toBlob({ + callback: (blob: Blob | null) => { + if (blob) { + resolve(blob); + } else { + reject(new ImageError("Не удалось создать изображение из Konva Stage")); + } + }, + }); + }); + } + + /** + * Экспортирует несколько панелей в ZIP архив + * (будет реализовано позже для batch download) + */ + async exportPanels(panels: Panel[]): Promise { + // TODO: Реализовать пакетный экспорт с использованием JSZip + return { + success: false, + error: "Пакетный экспорт еще не реализован", + }; + } + + /** + * Обработчик для скачивания одной панели + * Этот метод должен быть вызван из компонента PanelPreview с передачей konvaStage + */ + async handleDownload(panel: Panel, konvaStage: Stage): Promise { + const result = await this.exportPanel(panel, konvaStage); + if (!result.success) { + throw new Error(result.error || "Ошибка экспорта панели"); + } + } + + /** + * Обработчик для скачивания всех панелей + */ async handleDownloadAll(panels: Panel[]): Promise { - try { - setLoading(true); - for (const panel of panels) { - await exportPanelService.exportPanel(panel); - } - } catch (error) { - this.errorMessage = error instanceof Error ? error.message : "Ошибка экспорта панелей"; - } finally { - setLoading(false); + const result = await this.exportPanels(panels); + if (!result.success) { + throw new Error(result.error || "Ошибка экспорта панелей"); } } + + /** + * Получить сообщение об ошибке + */ + getErrorMessage(): string | null { + return null; // Реализовать при необходимости + } + + /** + * Установить сообщение об ошибке + */ + setErrorMessage(message: string): void { + // Реализовать при необходимости + console.error("ExportService error:", message); + } } -export const exportService = ExportService.getInstance(); +export const exportService = new ExportService(); diff --git a/src/services/imageService.ts b/src/services/imageService.ts index 91faacf..4885d87 100644 --- a/src/services/imageService.ts +++ b/src/services/imageService.ts @@ -41,6 +41,93 @@ export class ImageService { handleCropCancel(): void { setCurrentStep("text"); } + + async handleFileUpload(file: File): Promise { + // Convert file to base64 + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => + resolve({ + success: true, + image: reader.result as string, + }); + reader.onerror = () => + reject({ + success: false, + error: "Ошибка чтения файла", + }); + reader.readAsDataURL(file); + }); + } + + async handlePasteUpload(pasteEvent: ClipboardEvent): Promise { + const items = pasteEvent.clipboardData?.items; + if (!items || items.length === 0) { + return { + success: false, + error: "В буфере обмена не найдено изображений", + }; + } + + // Find image item + let imageItem: DataTransferItem | undefined = undefined; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + imageItem = items[i]; + break; + } + } + + if (!imageItem) { + return { + success: false, + error: "В буфере обмена не найдено изображений", + }; + } + + // Get file from clipboard item + const file = imageItem.getAsFile(); + if (!file) { + return { + success: false, + error: "Не удалось получить файл из буфера обмена", + }; + } + + return this.handleFileUpload(file); + } + + async handleUrlUpload(url: string): Promise { + try { + const response = await fetch(url); + if (!response.ok) { + return { + success: false, + error: "Не удалось загрузить изображение по URL", + }; + } + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => + resolve({ + success: true, + image: reader.result as string, + }); + reader.onerror = () => + reject({ + success: false, + error: "Ошибка чтения изображения", + }); + reader.readAsDataURL(blob); + }); + } catch (error) { + return { + success: false, + error: "Ошибка загрузки изображения по URL", + }; + } + } } export const imageService = ImageService.getInstance();