feat: implement save all
This commit is contained in:
@@ -1,34 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PANEL_SETTINGS, type SlideDirectionType } from "$lib/constants";
|
import { PANEL_SETTINGS } from "$lib/constants";
|
||||||
import { imageConfigState } from "$states/imageConfig.svelte";
|
import { imageConfigState } from "$states/imageConfig.svelte";
|
||||||
import { textConfigState } from "$states/textConfig.svelte";
|
import { textConfigState } from "$states/textConfig.svelte";
|
||||||
import { Image, Layer, Stage, Text } from "svelte-konva";
|
import { Image, Layer, Stage, Text } from "svelte-konva";
|
||||||
import { fly } from "svelte/transition";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
direction: SlideDirectionType;
|
stage: Stage | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { text, direction }: Props = $props();
|
let { text, stage = $bindable() }: Props = $props();
|
||||||
|
|
||||||
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
|
|
||||||
|
|
||||||
let image: HTMLImageElement | undefined = imageConfigState.image;
|
let image: HTMLImageElement | undefined = imageConfigState.image;
|
||||||
|
|
||||||
let { align, fontSize, fontFamily, paddingX, offsetY, color } = textConfigState;
|
let x = $derived(textConfigState.paddingX);
|
||||||
|
let y = $derived(10 + textConfigState.offsetY);
|
||||||
let x = $derived(paddingX);
|
let width = $derived(PANEL_SETTINGS.PANEL_WIDTH - 2 * textConfigState.paddingX);
|
||||||
let y = $derived(10 + offsetY);
|
|
||||||
let width = $derived(PANEL_SETTINGS.PANEL_WIDTH - 2 * paddingX);
|
|
||||||
let height = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
|
let height = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="konva-wrapper" in:fly={{ x: xDirection, duration: 300 }} out:fly={{ x: -xDirection, duration: 300 }}>
|
<Stage width={320} height={100} bind:this={stage}>
|
||||||
<Stage width={320} height={100}>
|
|
||||||
<Layer>
|
<Layer>
|
||||||
<Image {image}></Image>
|
<Image {image}></Image>
|
||||||
{$inspect(paddingX)}
|
|
||||||
<Text
|
<Text
|
||||||
{text}
|
{text}
|
||||||
{x}
|
{x}
|
||||||
@@ -43,18 +36,4 @@
|
|||||||
ellipsis={true}
|
ellipsis={true}
|
||||||
/>
|
/>
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.konva-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
|
||||||
|
import { textsState } from "$states/texts.svelte";
|
||||||
|
|
||||||
|
import Preview from "./Preview.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="outside-display">
|
||||||
|
{#each textsState.texts as { text, id }, idx (id)}
|
||||||
|
<Preview {text} bind:stage={konvaAllStagesState[idx]} />
|
||||||
|
{:else}
|
||||||
|
<p>No texts to preview</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.outside-display {
|
||||||
|
position: fixed;
|
||||||
|
top: -1000px;
|
||||||
|
left: -1000px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
|
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
|
||||||
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
|
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
|
||||||
import type { SlideDirectionType } from "$lib/constants";
|
import type { SlideDirectionType } from "$lib/constants";
|
||||||
|
import { PANEL_SETTINGS } from "$lib/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
current: number;
|
current: number;
|
||||||
@@ -24,6 +25,8 @@
|
|||||||
if (current < max - 1) current++;
|
if (current < max - 1) current++;
|
||||||
direction = "next";
|
direction = "next";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button icon={IconArrowLeft} type="mini" onclick={prev} disabled={isFirst} />
|
<Button icon={IconArrowLeft} type="mini" onclick={prev} disabled={isFirst} />
|
||||||
|
|||||||
@@ -4,16 +4,22 @@
|
|||||||
import Button from "$components/ui/Button.svelte";
|
import Button from "$components/ui/Button.svelte";
|
||||||
import IconDownload from "$components/ui/Icons/IconDownload.svelte";
|
import IconDownload from "$components/ui/Icons/IconDownload.svelte";
|
||||||
import IconEmpty from "$components/ui/Icons/IconEmpty.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 { textsState } from "$states/texts.svelte";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
import Preview from "./Preview.svelte";
|
import Preview from "./Preview.svelte";
|
||||||
|
import PreviewAll from "./PreviewAll.svelte";
|
||||||
import PreviewControls from "./PreviewControls.svelte";
|
import PreviewControls from "./PreviewControls.svelte";
|
||||||
|
|
||||||
let current: number = $state(0);
|
let current: number = $state(0);
|
||||||
let direction: SlideDirectionType = $state("next");
|
let direction: SlideDirectionType = $state("next");
|
||||||
|
|
||||||
|
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
$inspect(current, textsState.texts);
|
|
||||||
if (textsState.texts.length === 0) {
|
if (textsState.texts.length === 0) {
|
||||||
current = 0;
|
current = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -22,6 +28,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function downloadAll() {
|
||||||
|
let downloadItems: Array<DownloadItem> = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet panelTitle()}
|
{#snippet panelTitle()}
|
||||||
@@ -38,7 +60,13 @@
|
|||||||
{#if textsState.texts.length}
|
{#if textsState.texts.length}
|
||||||
{#key current}
|
{#key current}
|
||||||
{@const text = textsState?.texts[current]?.text}
|
{@const text = textsState?.texts[current]?.text}
|
||||||
<Preview {text} {direction} />
|
<div
|
||||||
|
class="konva-wrapper"
|
||||||
|
in:fly={{ x: xDirection, duration: 300 }}
|
||||||
|
out:fly={{ x: -xDirection, duration: 300 }}
|
||||||
|
>
|
||||||
|
<Preview {text} bind:stage={konvaStageState.stage} />
|
||||||
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -49,11 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<Button label="Скачать всё" icon={IconDownload} />
|
<Button label="Скачать всё" icon={IconDownload} onclick={downloadAll} />
|
||||||
<Button label="Скачать" type="outline" icon={IconDownload} />
|
<Button label="Скачать" type="outline" icon={IconDownload} onclick={downloadCurrent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<PreviewAll />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel-viewer {
|
.panel-viewer {
|
||||||
@@ -94,4 +123,15 @@
|
|||||||
.empty-state p {
|
.empty-state p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.konva-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<DownloadResult> {
|
||||||
|
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<DownloadItem>): Promise<DownloadResult> {
|
||||||
|
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<Blob> {
|
||||||
|
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();
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { Stage } from "svelte-konva";
|
||||||
|
|
||||||
|
export const konvaAllStagesState: Array<Stage> = $state([]);
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user