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();