diff --git a/src/components/panel/Preview.svelte b/src/components/panel/Preview.svelte
index fe9d55b..538f0b8 100644
--- a/src/components/panel/Preview.svelte
+++ b/src/components/panel/Preview.svelte
@@ -1,60 +1,39 @@
-
-
-
-
- {$inspect(paddingX)}
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/components/panel/PreviewAll.svelte b/src/components/panel/PreviewAll.svelte
new file mode 100644
index 0000000..dc3d1ad
--- /dev/null
+++ b/src/components/panel/PreviewAll.svelte
@@ -0,0 +1,22 @@
+
+
+
+ {#each textsState.texts as { text, id }, idx (id)}
+
+ {:else}
+
No texts to preview
+ {/each}
+
+
+
diff --git a/src/components/panel/PreviewControls.svelte b/src/components/panel/PreviewControls.svelte
index 6e4d335..c6ba9b1 100644
--- a/src/components/panel/PreviewControls.svelte
+++ b/src/components/panel/PreviewControls.svelte
@@ -3,6 +3,7 @@
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
import type { SlideDirectionType } from "$lib/constants";
+ import { PANEL_SETTINGS } from "$lib/constants";
interface Props {
current: number;
@@ -24,6 +25,8 @@
if (current < max - 1) current++;
direction = "next";
}
+
+ let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
diff --git a/src/components/panel/PreviewManager.svelte b/src/components/panel/PreviewManager.svelte
index 10f3df7..44a3e13 100644
--- a/src/components/panel/PreviewManager.svelte
+++ b/src/components/panel/PreviewManager.svelte
@@ -4,16 +4,22 @@
import Button from "$components/ui/Button.svelte";
import IconDownload from "$components/ui/Icons/IconDownload.svelte";
import IconEmpty from "$components/ui/Icons/IconEmpty.svelte";
- import type { SlideDirectionType } from "$lib/constants";
+ import { PANEL_SETTINGS, type SlideDirectionType } from "$lib/constants";
+ import { downloadService, type DownloadItem } from "$services/downloadService";
+ import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
+ import { konvaStageState } from "$states/konvaStage.svelte";
import { textsState } from "$states/texts.svelte";
+ import { fly } from "svelte/transition";
import Preview from "./Preview.svelte";
+ import PreviewAll from "./PreviewAll.svelte";
import PreviewControls from "./PreviewControls.svelte";
let current: number = $state(0);
let direction: SlideDirectionType = $state("next");
+ let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
+
$effect(() => {
- $inspect(current, textsState.texts);
if (textsState.texts.length === 0) {
current = 0;
} else {
@@ -22,6 +28,22 @@
}
}
});
+
+ function downloadAll() {
+ let downloadItems: Array = textsState.texts.map((text, idx) => ({
+ filename: text.text,
+ stage: konvaAllStagesState[idx]?.node,
+ }));
+
+ downloadService.downloadAll(downloadItems);
+ }
+
+ function downloadCurrent() {
+ let node = konvaStageState.stage?.node;
+ if (node) {
+ downloadService.downloadPanel(node, textsState.texts[current].text);
+ }
+ }
{#snippet panelTitle()}
@@ -38,7 +60,13 @@
{#if textsState.texts.length}
{#key current}
{@const text = textsState?.texts[current]?.text}
-
+
{/key}
{:else}
@@ -49,11 +77,12 @@
-
-
+
+
+
diff --git a/src/lib/error.types.ts b/src/lib/error.types.ts
new file mode 100644
index 0000000..09be9b1
--- /dev/null
+++ b/src/lib/error.types.ts
@@ -0,0 +1,36 @@
+export class AppError extends Error {
+ constructor(
+ message: string,
+ public code: string,
+ public details?: unknown,
+ ) {
+ super(message);
+ this.name = "AppError";
+ }
+}
+
+export class ImageError extends AppError {
+ constructor(message: string) {
+ super(message, "IMAGE_ERROR");
+ }
+}
+
+export class TextError extends AppError {
+ constructor(message: string) {
+ super(message, "TEXT_ERROR");
+ }
+}
+
+export class CanvasError extends AppError {
+ constructor(message: string) {
+ super(message, "CANVAS_ERROR");
+ }
+}
+
+export class StorageError extends AppError {
+ constructor(message: string) {
+ super(message, "STORAGE_ERROR");
+ }
+}
+
+export type ErrorType = AppError | ImageError | TextError | CanvasError | StorageError;
diff --git a/src/lib/utils/errorUtils.ts b/src/lib/utils/errorUtils.ts
new file mode 100644
index 0000000..ee180a1
--- /dev/null
+++ b/src/lib/utils/errorUtils.ts
@@ -0,0 +1,26 @@
+import { AppError } from "$lib/error.types";
+
+export function formatError(error: unknown, defaultMessage: string = "Произошла ошибка"): string {
+ if (error instanceof AppError || error instanceof Error) {
+ return `${defaultMessage}: ${error.message}`;
+ }
+
+ if (typeof error === "string") {
+ return `${defaultMessage}: ${error}`;
+ }
+
+ return `${defaultMessage}: Произошла неизвестная ошибка`;
+}
+
+export function createError(message: string, code: string, recoverable: boolean = true, details?: unknown): AppError {
+ return new AppError(message, code, recoverable);
+}
+
+export function logError(error: unknown, context?: string): void {
+ console.error("Error occurred:", {
+ error,
+ context,
+ timestamp: new Date().toISOString(),
+ stack: error instanceof Error ? error.stack : undefined,
+ });
+}
diff --git a/src/services/downloadService.ts b/src/services/downloadService.ts
new file mode 100644
index 0000000..c3f2bd0
--- /dev/null
+++ b/src/services/downloadService.ts
@@ -0,0 +1,83 @@
+import { ImageError } from "$lib/error.types";
+import { formatError, logError } from "$lib/utils/errorUtils";
+import { saveAs } from "file-saver";
+import JSZip from "jszip";
+import { Stage } from "konva/lib/Stage";
+
+export type DownloadResult =
+ | {
+ success: true;
+ }
+ | {
+ success: false;
+ error: string;
+ };
+
+export interface DownloadItem {
+ filename: string;
+ stage: Stage;
+}
+
+export class DownloadService {
+ async downloadPanel(konvaStage: Stage, label: string): Promise {
+ try {
+ const blob = await this.stageToBlob(konvaStage);
+
+ const filename = `${label}.png`;
+ saveAs(blob, filename);
+
+ return {
+ success: true,
+ };
+ } catch (error) {
+ logError(error, "Ошибка сохранения панели");
+ return {
+ success: false,
+ error: formatError(error, "Ошибка сохранения панели"),
+ };
+ }
+ }
+
+ async downloadAll(panels: Array): Promise {
+ try {
+ const zip = new JSZip();
+ for (let panel of panels) {
+ const blob = await this.stageToBlob(panel.stage);
+ zip.file(`${panel.filename}.png`, blob);
+ }
+
+ const zipBlob = await zip.generateAsync({ type: "blob" });
+ saveAs(zipBlob, "panels.zip");
+
+ return {
+ success: true,
+ };
+ } catch (error) {
+ logError(error, "Ошибка сохранения архива");
+ return {
+ success: false,
+ error: formatError(error, "Ошибка сохранения архива"),
+ };
+ }
+ }
+
+ private async stageToBlob(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"));
+ }
+ },
+ });
+ });
+ }
+}
+
+export const downloadService = new DownloadService();
diff --git a/src/states/konvaAllStages.svelte.ts b/src/states/konvaAllStages.svelte.ts
new file mode 100644
index 0000000..915f31d
--- /dev/null
+++ b/src/states/konvaAllStages.svelte.ts
@@ -0,0 +1,3 @@
+import type { Stage } from "svelte-konva";
+
+export const konvaAllStagesState: Array = $state([]);
diff --git a/src/states/konvaStage.svelte.ts b/src/states/konvaStage.svelte.ts
new file mode 100644
index 0000000..9ef2781
--- /dev/null
+++ b/src/states/konvaStage.svelte.ts
@@ -0,0 +1,16 @@
+import type { Stage } from "svelte-konva";
+
+function createState() {
+ let stage: Stage | undefined = $state(undefined);
+
+ return {
+ get stage(): Stage | undefined {
+ return stage;
+ },
+ set stage(newStage: Stage) {
+ stage = newStage;
+ },
+ };
+}
+
+export const konvaStageState = createState();