feat: start implement panels rendering, remove unused files

This commit is contained in:
2026-02-06 21:27:52 +05:00
parent 571fd9bf25
commit 886ef73474
20 changed files with 176 additions and 826 deletions
+26 -3
View File
@@ -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>
+1 -1
View File
@@ -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;
+11 -4
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+11
View File
@@ -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

-30
View File
@@ -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";
}
-16
View File
@@ -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 [];
}
}
-37
View File
@@ -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;
-63
View File
@@ -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();
});
}
-102
View File
@@ -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,
};
}
-137
View File
@@ -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();
-98
View File
@@ -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();
-130
View File
@@ -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();
-71
View File
@@ -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();
+120
View File
@@ -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 -1
View File
@@ -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;
-88
View File
@@ -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(),
};
};
-44
View File
@@ -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,
});
};