feat: start implement panels rendering, remove unused files
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SlideDirection } from "$lib/types/utils";
|
import type { SlideDirection } from "$lib/util-types";
|
||||||
|
import { imageConfigState } from "$states/image.svelte";
|
||||||
|
import { Image, Layer, Stage, Text } from "svelte-konva";
|
||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -10,8 +12,29 @@
|
|||||||
let { text, direction }: Props = $props();
|
let { text, direction }: Props = $props();
|
||||||
|
|
||||||
let xDirection = $derived(direction == "next" ? 320 : -320);
|
let xDirection = $derived(direction == "next" ? 320 : -320);
|
||||||
|
|
||||||
|
let image: HTMLImageElement | undefined = imageConfigState.image;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div in:fly={{ x: xDirection, duration: 300 }} out:fly={{ x: -xDirection, duration: 300 }}>
|
<div class="konva-wrapper" in:fly={{ x: xDirection, duration: 300 }} out:fly={{ x: -xDirection, duration: 300 }}>
|
||||||
{text}
|
<Stage width={320} height={100}>
|
||||||
|
<Layer>
|
||||||
|
<Image {image}></Image>
|
||||||
|
{$inspect(image)}
|
||||||
|
<Text {text} x={10} y={10} fontsize="32" fill="white"></Text>
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.konva-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Button from "$components/ui/Button.svelte";
|
import Button from "$components/ui/Button.svelte";
|
||||||
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 { SlideDirection } from "$lib/types/utils";
|
import type { SlideDirection } from "$lib/util-types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
current: number;
|
current: number;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
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 { SlideDirection } from "$lib/types/utils";
|
import type { SlideDirection } from "$lib/util-types";
|
||||||
import { textsState } from "$states/texts.svelte";
|
import { textsState } from "$states/texts.svelte";
|
||||||
import Preview from "./Preview.svelte";
|
import Preview from "./Preview.svelte";
|
||||||
import PreviewControls from "./PreviewControls.svelte";
|
import PreviewControls from "./PreviewControls.svelte";
|
||||||
@@ -14,7 +14,13 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
$inspect(current, textsState.texts);
|
$inspect(current, textsState.texts);
|
||||||
if (current > textsState.texts.length - 1) current = textsState.texts.length - 1;
|
if (textsState.texts.length === 0) {
|
||||||
|
current = 0;
|
||||||
|
} else {
|
||||||
|
if (current > textsState.texts.length - 1) {
|
||||||
|
current = textsState.texts.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,9 +36,9 @@
|
|||||||
<div class="panel-viewer">
|
<div class="panel-viewer">
|
||||||
<div class="panel-display">
|
<div class="panel-display">
|
||||||
{#if textsState.texts.length}
|
{#if textsState.texts.length}
|
||||||
<!-- {@const text = } -->
|
|
||||||
{#key current}
|
{#key current}
|
||||||
<Preview text={textsState.texts[current].text} {direction} />
|
{@const text = textsState?.texts[current]?.text}
|
||||||
|
<Preview {text} {direction} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -64,6 +70,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.panel-actions {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
let { text }: Props = $props();
|
let { text }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="badge" id="panelCount">{text}</span>
|
<span class="badge">{text}</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.badge {
|
.badge {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M4.215 7.266h0.003a0.545 0.545 0 0 0 0.539 0.544v0H19.243v0a0.545 0.545 0 0 0 0.542 -0.544h0.002V4.756a0.545 0.545 0 0 0 -0.545 -0.544H4.757a0.545 0.545 0 0 0 -0.545 0.545c0 0.009 0.002 0.018 0.003 0.028z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19.243 10.201H4.757a0.545 0.545 0 0 0 -0.545 0.545c0 0.009 0.002 0.018 0.003 0.028v2.482h0.003a0.545 0.545 0 0 0 0.539 0.544v0h14.486v0a0.545 0.545 0 0 0 0.542 -0.544h0.002V10.745a0.545 0.545 0 0 0 -0.545 -0.544"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19.243 16.189H4.757a0.545 0.545 0 0 0 -0.545 0.545c0 0.009 0.002 0.018 0.003 0.028v2.482h0.003a0.545 0.545 0 0 0 0.539 0.544v0h14.486v0a0.545 0.545 0 0 0 0.542 -0.545h0.002V16.732a0.545 0.545 0 0 0 -0.545 -0.543"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 747 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1">
|
||||||
|
<path
|
||||||
|
d="M8.293 14.293a1 1 0 0 1 1.414 0L12 16.586l2.293-2.293a1 1 0 0 1 1.414 1.414l-3 3a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 0-1.414ZM11.293 5.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1-1.414 1.414L12 7.414 9.707 9.707a1 1 0 0 1-1.414-1.414l3-3Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 341 B |
@@ -1,30 +0,0 @@
|
|||||||
import { building } from "$app/environment";
|
|
||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export function loadFonts() {
|
|
||||||
try {
|
|
||||||
const fontsDir = join(process.cwd(), "static", "fonts");
|
|
||||||
const files = readdirSync(fontsDir);
|
|
||||||
|
|
||||||
let fonts = files
|
|
||||||
.filter((file) => /\.(woff2|woff|ttf|otf)$/i.test(file))
|
|
||||||
.map((file) => ({
|
|
||||||
name: file.replace(/\.[^/.]+$/, ""),
|
|
||||||
file,
|
|
||||||
url: `/fonts/${file}`,
|
|
||||||
format: getFormat(file),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return fonts;
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormat(filename: string) {
|
|
||||||
if (filename.endsWith(".woff2")) return "woff2";
|
|
||||||
if (filename.endsWith(".woff")) return "woff";
|
|
||||||
if (filename.endsWith(".ttf")) return "truetype";
|
|
||||||
return "opentype";
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { building } from "$app/environment";
|
|
||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export function loadImages() {
|
|
||||||
try {
|
|
||||||
const imagesDir = join(process.cwd(), "static", "backgrounds");
|
|
||||||
const files = readdirSync(imagesDir);
|
|
||||||
|
|
||||||
let fonts = files.filter((file) => /\.(png|jpg)$/i.test(file));
|
|
||||||
|
|
||||||
return fonts;
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export class AppError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public code: string,
|
|
||||||
public recoverable: boolean = true,
|
|
||||||
public details?: unknown,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "AppError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ImageError extends AppError {
|
|
||||||
constructor(message: string, recoverable: boolean = true) {
|
|
||||||
super(message, "IMAGE_ERROR", recoverable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TextError extends AppError {
|
|
||||||
constructor(message: string, recoverable: boolean = true) {
|
|
||||||
super(message, "TEXT_ERROR", recoverable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CanvasError extends AppError {
|
|
||||||
constructor(message: string, recoverable: boolean = true) {
|
|
||||||
super(message, "CANVAS_ERROR", recoverable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StorageError extends AppError {
|
|
||||||
constructor(message: string, recoverable: boolean = true) {
|
|
||||||
super(message, "STORAGE_ERROR", recoverable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ErrorType = AppError | ImageError | TextError | CanvasError | StorageError;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { ERROR_HANDLING } from "../constants";
|
|
||||||
import { AppError } from "../types/errors";
|
|
||||||
|
|
||||||
export function handleError(error: unknown, defaultMessage: string = "Произошла ошибка"): string {
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return error.recoverable ? `Ошибка: ${error.message}. Попробуйте снова.` : `Критическая ошибка: ${error.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return `${defaultMessage}: ${error.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "string") {
|
|
||||||
return `${defaultMessage}: ${error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${defaultMessage}: Произошла неизвестная ошибка`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRecoverableError(error: unknown): boolean {
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
return error.recoverable;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createError(message: string, code: string, recoverable: boolean = true, details?: unknown): AppError {
|
|
||||||
return new AppError(message, code, recoverable, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function retryOperation<T>(
|
|
||||||
operation: () => Promise<T>,
|
|
||||||
maxRetries: number = ERROR_HANDLING.MAX_RETRIES,
|
|
||||||
delayMs: number = ERROR_HANDLING.RETRY_DELAY_MS,
|
|
||||||
): Promise<T> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let attempt = 0;
|
|
||||||
|
|
||||||
const attemptOperation = () => {
|
|
||||||
attempt++;
|
|
||||||
operation()
|
|
||||||
.then(resolve)
|
|
||||||
.catch((error) => {
|
|
||||||
if (attempt >= maxRetries) {
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
setTimeout(attemptOperation, delayMs * attempt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
attemptOperation();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { IMAGE_SETTINGS } from "../constants";
|
|
||||||
import { ImageError } from "../types/errors";
|
|
||||||
|
|
||||||
export const MAX_FILE_SIZE = IMAGE_SETTINGS.MAX_FILE_SIZE;
|
|
||||||
export const SUPPORTED_FORMATS = IMAGE_SETTINGS.SUPPORTED_FORMATS;
|
|
||||||
|
|
||||||
export type ValidationResult =
|
|
||||||
| {
|
|
||||||
isValid: true;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
isValid: false;
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function validateFileSize(file: File): ValidationResult {
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
error: `Файл слишком большой. Максимальный размер: ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { isValid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateFileType(file: File): ValidationResult {
|
|
||||||
if (!SUPPORTED_FORMATS.includes(file.type as any)) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
error: `Неподдерживаемый формат. Допустимые форматы: ${SUPPORTED_FORMATS.join(", ")}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { isValid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateImageFile(file: File): ValidationResult {
|
|
||||||
const sizeValidation = validateFileSize(file);
|
|
||||||
if (!sizeValidation.isValid) {
|
|
||||||
return sizeValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeValidation = validateFileType(file);
|
|
||||||
if (!typeValidation.isValid) {
|
|
||||||
return typeValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateImageUrl(url: string): ValidationResult {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return { isValid: true };
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
error: "Неверный URL формат",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateImageElement(img: HTMLImageElement): ValidationResult {
|
|
||||||
if (!img.naturalWidth || !img.naturalHeight) {
|
|
||||||
return {
|
|
||||||
isValid: false,
|
|
||||||
error: "Изображение не может быть загружено или повреждено",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { isValid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadImage(src: string): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.crossOrigin = "anonymous";
|
|
||||||
|
|
||||||
img.onload = () => resolve(img);
|
|
||||||
img.onerror = () => reject(new ImageError("Не удалось загрузить изображение"));
|
|
||||||
|
|
||||||
img.src = src;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageDimensions(img: HTMLImageElement): { width: number; height: number } {
|
|
||||||
return {
|
|
||||||
width: img.naturalWidth,
|
|
||||||
height: img.naturalHeight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateAspectRatioFit(
|
|
||||||
srcWidth: number,
|
|
||||||
srcHeight: number,
|
|
||||||
maxWidth: number,
|
|
||||||
maxHeight: number,
|
|
||||||
): { width: number; height: number } {
|
|
||||||
const ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
|
|
||||||
return {
|
|
||||||
width: srcWidth * ratio,
|
|
||||||
height: srcHeight * ratio,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { PANEL_SETTINGS } from "../constants";
|
|
||||||
import type { Panel } from "../types/panel";
|
|
||||||
import { handleError, logError } from "./errorHandler";
|
|
||||||
|
|
||||||
const STORAGE_KEY = PANEL_SETTINGS.STORAGE_KEY;
|
|
||||||
const MAX_PANELS = PANEL_SETTINGS.MAX_PANELS_COUNT;
|
|
||||||
|
|
||||||
export interface StorageResult {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PanelStorage {
|
|
||||||
savePanel(panel: Panel): StorageResult {
|
|
||||||
try {
|
|
||||||
const panels = this.getAllPanels();
|
|
||||||
|
|
||||||
const existingIndex = panels.findIndex((p) => p.id === panel.id);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
panels[existingIndex] = panel;
|
|
||||||
} else {
|
|
||||||
panels.unshift(panel);
|
|
||||||
|
|
||||||
if (panels.length > MAX_PANELS) {
|
|
||||||
panels.length = MAX_PANELS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(panels));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to save panel");
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: handleError(error, "Ошибка сохранения панели"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllPanels(): Panel[] {
|
|
||||||
try {
|
|
||||||
const data = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (!data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const panels: Panel[] = JSON.parse(data);
|
|
||||||
|
|
||||||
// Convert date strings back to Date objects and validate
|
|
||||||
return panels
|
|
||||||
.map((panel) => ({
|
|
||||||
...panel,
|
|
||||||
createdAt: new Date(panel.createdAt),
|
|
||||||
updatedAt: new Date(panel.updatedAt),
|
|
||||||
}))
|
|
||||||
.filter((panel) => this.validatePanel(panel));
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to load panels");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelById(id: string): Panel | undefined {
|
|
||||||
try {
|
|
||||||
const panels = this.getAllPanels();
|
|
||||||
return panels.find((p) => p.id === id) || undefined;
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to get panel by id");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePanel(id: string): StorageResult {
|
|
||||||
try {
|
|
||||||
const panels = this.getAllPanels();
|
|
||||||
const filteredPanels = panels.filter((p) => p.id !== id);
|
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(filteredPanels));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to delete panel");
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: handleError(error, "Ошибка удаления панели"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAll(): StorageResult {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to clear panels");
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: handleError(error, "Ошибка очистки хранилища"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private validatePanel(panel: any): panel is Panel {
|
|
||||||
return (
|
|
||||||
typeof panel === "object" &&
|
|
||||||
typeof panel.id === "string" &&
|
|
||||||
typeof panel.backgroundImage === "string" &&
|
|
||||||
typeof panel.text === "object" &&
|
|
||||||
typeof panel.text?.id === "string" &&
|
|
||||||
typeof panel.text?.text === "string" &&
|
|
||||||
typeof panel.height === "number" &&
|
|
||||||
panel.height > 0 &&
|
|
||||||
panel.height <= PANEL_SETTINGS.PANEL_HEIGHT_MAX &&
|
|
||||||
panel.createdAt instanceof Date &&
|
|
||||||
panel.updatedAt instanceof Date
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelCount(): number {
|
|
||||||
return this.getAllPanels().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSpaceForNewPanel(): boolean {
|
|
||||||
return this.getPanelCount() < MAX_PANELS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const panelStorage = new PanelStorage();
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import pkg from "file-saver";
|
|
||||||
import type { Stage } from "konva/lib/Stage";
|
|
||||||
import { ImageError } from "../lib/types/errors";
|
|
||||||
import type { Panel } from "../lib/types/panel";
|
|
||||||
import { handleError, logError } from "../lib/utils/errorHandler";
|
|
||||||
const { saveAs } = pkg;
|
|
||||||
|
|
||||||
export interface ExportResult {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExportService {
|
|
||||||
async exportPanel(panel: Panel, konvaStage: Stage, filename?: string): Promise<ExportResult> {
|
|
||||||
try {
|
|
||||||
if (!konvaStage) {
|
|
||||||
throw new ImageError("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) {
|
|
||||||
logError(error, "Panel export failed");
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: handleError(error, "Ошибка экспорта панели"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async exportKonvaStageToBlob(konvaStage: Stage): Promise<Blob | null> {
|
|
||||||
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 архив
|
|
||||||
*/
|
|
||||||
async exportPanels(panels: Panel[]): Promise<ExportResult> {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "Пакетный экспорт еще не реализован",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработчик для скачивания одной панели
|
|
||||||
*/
|
|
||||||
async handleDownload(panel: Panel, konvaStage: Stage): Promise<void> {
|
|
||||||
const result = await this.exportPanel(panel, konvaStage);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || "Ошибка экспорта панели");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработчик для скачивания всех панелей
|
|
||||||
*/
|
|
||||||
async handleDownloadAll(panels: Panel[]): Promise<void> {
|
|
||||||
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 = new ExportService();
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { PANEL_SETTINGS } from "../lib/constants";
|
|
||||||
import { setCurrentStep } from "../stores/uiStore";
|
|
||||||
|
|
||||||
export class ImageService {
|
|
||||||
private static instance: ImageService;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static getInstance(): ImageService {
|
|
||||||
if (!ImageService.instance) {
|
|
||||||
ImageService.instance = new ImageService();
|
|
||||||
}
|
|
||||||
return ImageService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadDefaultBackground(): Promise<string> {
|
|
||||||
const defaultBackground = PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE;
|
|
||||||
const response = await fetch(defaultBackground);
|
|
||||||
if (!response.ok) throw new Error("Не удалось загрузить фоновое изображение");
|
|
||||||
const blob = await response.blob();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImageUpload(image: string): void {
|
|
||||||
setCurrentStep("crop");
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUploadNewImage(): void {
|
|
||||||
setCurrentStep("upload");
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCropComplete(croppedImage: string): void {
|
|
||||||
setCurrentStep("text");
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCropCancel(): void {
|
|
||||||
setCurrentStep("text");
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleFileUpload(file: File): Promise<any> {
|
|
||||||
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<any> {
|
|
||||||
const items = pasteEvent.clipboardData?.items;
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "В буфере обмена не найдено изображений",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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: "В буфере обмена не найдено изображений",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = imageItem.getAsFile();
|
|
||||||
if (!file) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "Не удалось получить файл из буфера обмена",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.handleFileUpload(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleUrlUpload(url: string): Promise<any> {
|
|
||||||
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();
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { PANEL_SETTINGS } from "../lib/constants";
|
|
||||||
import type { Panel, TextItem } from "../lib/types/panel";
|
|
||||||
import { createPanelFromText, updatePanelText } from "../stores/panelStore";
|
|
||||||
|
|
||||||
export class PanelService {
|
|
||||||
private static instance: PanelService;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static getInstance(): PanelService {
|
|
||||||
if (!PanelService.instance) {
|
|
||||||
PanelService.instance = new PanelService();
|
|
||||||
}
|
|
||||||
return PanelService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateText(text: string): boolean {
|
|
||||||
return text.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDuplicateText(texts: Array<{ id: string; text: string }>, text: string, excludeId?: string): boolean {
|
|
||||||
return texts.some((t) => t.id !== excludeId && t.text === text);
|
|
||||||
}
|
|
||||||
|
|
||||||
addText(texts: Array<{ id: string; text: string }>, text: string): Array<{ id: string; text: string }> {
|
|
||||||
if (!this.validateText(text)) return texts;
|
|
||||||
if (this.isDuplicateText(texts, text)) return texts;
|
|
||||||
|
|
||||||
return [...texts, { id: crypto.randomUUID(), text }];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateText(
|
|
||||||
texts: Array<{ id: string; text: string }>,
|
|
||||||
id: string,
|
|
||||||
newText: string,
|
|
||||||
): Array<{ id: string; text: string }> {
|
|
||||||
if (!this.validateText(newText)) return texts;
|
|
||||||
if (this.isDuplicateText(texts, newText, id)) return texts;
|
|
||||||
|
|
||||||
return texts.map((t) => (t.id === id ? { ...t, text: newText } : t));
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteText(texts: Array<{ id: string; text: string }>, id: string): Array<{ id: string; text: string }> {
|
|
||||||
return texts.filter((t) => t.id !== id);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePanelsFromTexts(
|
|
||||||
texts: Array<{ id: string; text: string }>,
|
|
||||||
panels: Panel[],
|
|
||||||
backgroundImage: string,
|
|
||||||
textSettings?: Partial<TextItem>,
|
|
||||||
): Panel[] {
|
|
||||||
return texts.map((textItem) => {
|
|
||||||
const existingPanel = panels.find((p) => p.text.text === textItem.text);
|
|
||||||
if (existingPanel) {
|
|
||||||
return updatePanelText(existingPanel, textItem.text);
|
|
||||||
}
|
|
||||||
return createPanelFromText(backgroundImage, textItem.text, PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT, textSettings);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePanelsBackground(panels: Panel[], newBackground: string): Panel[] {
|
|
||||||
return panels.map((panel) => ({
|
|
||||||
...panel,
|
|
||||||
backgroundImage: newBackground,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const panelService = PanelService.getInstance();
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
export type ImageConfig = {
|
||||||
|
image: HTMLImageElement | undefined;
|
||||||
|
imageLink: string;
|
||||||
|
imageReady: boolean;
|
||||||
|
cropLeft: number;
|
||||||
|
cropTop: number;
|
||||||
|
cropRight: number;
|
||||||
|
cropBottom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createState() {
|
||||||
|
const defaults: ImageConfig = {
|
||||||
|
image: undefined,
|
||||||
|
imageLink: "",
|
||||||
|
imageReady: false,
|
||||||
|
cropLeft: 0,
|
||||||
|
cropTop: 0,
|
||||||
|
cropRight: 0,
|
||||||
|
cropBottom: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let state: ImageConfig = $state({ ...defaults });
|
||||||
|
let currentAbortController: AbortController | null = null;
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (currentAbortController) {
|
||||||
|
currentAbortController.abort();
|
||||||
|
currentAbortController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.image) {
|
||||||
|
state.image.onload = null;
|
||||||
|
state.image.onerror = null;
|
||||||
|
state.image.src = "";
|
||||||
|
state.image = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImageByLink(link: string): Promise<void> {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
state.imageReady = false;
|
||||||
|
state.imageLink = link;
|
||||||
|
|
||||||
|
currentAbortController = new AbortController();
|
||||||
|
const { signal } = currentAbortController;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (signal.aborted) {
|
||||||
|
reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
img.onload = null;
|
||||||
|
img.onerror = null;
|
||||||
|
|
||||||
|
state.image = img;
|
||||||
|
state.imageReady = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = (error) => {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
img.onload = null;
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = "";
|
||||||
|
|
||||||
|
state.imageReady = false;
|
||||||
|
reject(new Error(`Failed to load image: ${link}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
img.onload = null;
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = "";
|
||||||
|
reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
img.src = link;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await uploadImageByLink("./backgrounds/b1.jpg");
|
||||||
|
|
||||||
|
return {
|
||||||
|
get image() {
|
||||||
|
return state.image;
|
||||||
|
},
|
||||||
|
get imageReady() {
|
||||||
|
return state.imageReady;
|
||||||
|
},
|
||||||
|
get imageLink() {
|
||||||
|
return state.imageLink;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadImageByLink,
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
cleanup();
|
||||||
|
Object.assign(state, defaults);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
cleanup();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imageConfigState = await createState();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HexColor, TextAlign } from "$lib/types/text";
|
import type { HexColor, TextAlign } from "$lib/util-types";
|
||||||
|
|
||||||
export type TextConfig = {
|
export type TextConfig = {
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { PANEL_SETTINGS, TYPOGRAPHY } from "../lib/constants";
|
|
||||||
import { type Panel, type TextItem } from "../lib/types/panel";
|
|
||||||
|
|
||||||
export const panelStore: Writable<Panel | undefined> = writable(undefined);
|
|
||||||
|
|
||||||
export const textSettingsStore = writable<Partial<TextItem>>({
|
|
||||||
fontSize: TYPOGRAPHY.FONT_SIZE_DEFAULT,
|
|
||||||
fontFamily: TYPOGRAPHY.FONT_FAMILY_DEFAULT,
|
|
||||||
color: TYPOGRAPHY.TEXT_COLOR_DEFAULT,
|
|
||||||
textAlign: TYPOGRAPHY.TEXT_ALIGN_LEFT,
|
|
||||||
paddingX: TYPOGRAPHY.PADDING_X_DEFAULT,
|
|
||||||
verticalOffset: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateAllTextSettings = (settings: Partial<TextItem>) => {
|
|
||||||
textSettingsStore.update((current) => ({ ...current, ...settings }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createEmptyPanel = (height: number = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT): Panel => {
|
|
||||||
const defaultText: TextItem = {
|
|
||||||
id: uuidv4(),
|
|
||||||
text: "",
|
|
||||||
fontSize: TYPOGRAPHY.FONT_SIZE_DEFAULT,
|
|
||||||
fontFamily: TYPOGRAPHY.FONT_FAMILY_DEFAULT,
|
|
||||||
color: TYPOGRAPHY.TEXT_COLOR_DEFAULT,
|
|
||||||
textAlign: TYPOGRAPHY.TEXT_ALIGN_CENTER,
|
|
||||||
paddingX: TYPOGRAPHY.PADDING_X_DEFAULT,
|
|
||||||
verticalOffset: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: uuidv4(),
|
|
||||||
backgroundImage: "",
|
|
||||||
text: defaultText,
|
|
||||||
height,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePanel = (panel: Panel, updates: Partial<Panel>): Panel => {
|
|
||||||
return {
|
|
||||||
...panel,
|
|
||||||
...updates,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePanelText = (panel: Panel, text: string): Panel => {
|
|
||||||
return updatePanel(panel, {
|
|
||||||
text: { ...panel.text, text },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTextProperties = (panel: Panel, updates: Partial<TextItem>): Panel => {
|
|
||||||
return updatePanel(panel, {
|
|
||||||
text: { ...panel.text, ...updates },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPanelFromText = (
|
|
||||||
backgroundImage: string,
|
|
||||||
text: string,
|
|
||||||
height: number = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT,
|
|
||||||
textSettings?: Partial<TextItem>,
|
|
||||||
): Panel => {
|
|
||||||
const newText: TextItem = {
|
|
||||||
id: uuidv4(),
|
|
||||||
text,
|
|
||||||
fontSize: textSettings?.fontSize ?? TYPOGRAPHY.FONT_SIZE_DEFAULT,
|
|
||||||
fontFamily: textSettings?.fontFamily ?? TYPOGRAPHY.FONT_FAMILY_DEFAULT,
|
|
||||||
color: textSettings?.color ?? TYPOGRAPHY.TEXT_COLOR_DEFAULT,
|
|
||||||
textAlign: textSettings?.textAlign ?? TYPOGRAPHY.TEXT_ALIGN_CENTER,
|
|
||||||
paddingX: textSettings?.paddingX ?? TYPOGRAPHY.PADDING_X_DEFAULT,
|
|
||||||
verticalOffset: textSettings?.verticalOffset ?? 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: uuidv4(),
|
|
||||||
backgroundImage,
|
|
||||||
text: newText,
|
|
||||||
height,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
|
||||||
import { type UIState } from "../lib/types/panel";
|
|
||||||
|
|
||||||
export const uiStore: Writable<UIState> = writable({
|
|
||||||
isLoading: false,
|
|
||||||
error: undefined,
|
|
||||||
currentStep: "text",
|
|
||||||
showCropModal: false,
|
|
||||||
showTextManager: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setLoading = (loading: boolean): void => {
|
|
||||||
uiStore.update((state) => ({ ...state, isLoading: loading }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setError = (error: string | undefined): void => {
|
|
||||||
uiStore.update((state) => ({ ...state, error }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearError = (): void => {
|
|
||||||
setError(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setCurrentStep = (step: UIState["currentStep"]): void => {
|
|
||||||
uiStore.update((state) => ({ ...state, currentStep: step }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showCropModal = (show: boolean): void => {
|
|
||||||
uiStore.update((state) => ({ ...state, showCropModal: show }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showTextManager = (show: boolean): void => {
|
|
||||||
uiStore.update((state) => ({ ...state, showTextManager: show }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetUI = (): void => {
|
|
||||||
uiStore.set({
|
|
||||||
isLoading: false,
|
|
||||||
error: undefined,
|
|
||||||
currentStep: "text",
|
|
||||||
showCropModal: false,
|
|
||||||
showTextManager: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user