feat: continue impemelent mvp components and stores

This commit is contained in:
2026-02-03 21:54:10 +05:00
parent ac864e3791
commit f317ac1030
7 changed files with 1307 additions and 19 deletions
+96
View File
@@ -0,0 +1,96 @@
import { saveAs } from "file-saver";
import type { Panel } from "../types/panel";
import { ImageError } from "../types/errors";
import { handleError, logError } from "../utils/errorHandler";
export interface ExportResult {
success: boolean;
error?: string;
}
export class ExportService {
/**
* Экспортирует панель в изображение и сохраняет её
*/
async exportPanel(panel: Panel, filename?: string): Promise<ExportResult> {
try {
if (!panel.backgroundImage) {
throw new ImageError("Нет фонового изображения для экспорта");
}
// Создаем canvas для экспорта
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new ImageError("Не удалось создать контекст canvas");
}
// Устанавливаем размеры canvas
canvas.width = 320;
canvas.height = panel.height;
// Загружаем фоновое изображение
const bgImage = await this.loadImage(panel.backgroundImage);
// Рисуем фон
ctx.drawImage(bgImage, 0, 0, 320, panel.height);
// Рисуем текст
panel.texts.forEach((textItem) => {
ctx.font = `${textItem.fontSize}px ${textItem.fontFamily}`;
ctx.fillStyle = textItem.color;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(textItem.text, textItem.x, textItem.y);
});
// Конвертируем в blob и сохраняем
canvas.toBlob((blob) => {
if (!blob) {
throw new ImageError("Не удалось создать изображение");
}
const defaultFilename = `twitch-panel-${panel.id}.png`;
saveAs(blob, filename || defaultFilename);
}, "image/png");
return {
success: true,
};
} catch (error) {
logError(error, "Panel export failed");
return {
success: false,
error: handleError(error, "Ошибка экспорта панели"),
};
}
}
/**
* Загружает изображение из base64 строки
*/
private loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new ImageError("Не удалось загрузить изображение"));
img.src = src;
});
}
/**
* Экспортирует несколько панелей в ZIP архив
* (будет реализовано позже для batch download)
*/
async exportPanels(panels: Panel[]): Promise<ExportResult> {
// TODO: Реализовать пакетный экспорт с использованием JSZip
return {
success: false,
error: "Пакетный экспорт еще не реализован",
};
}
}
export const exportService = new ExportService();
+158
View File
@@ -0,0 +1,158 @@
import type { Panel } from "../types/panel";
import { ImageError } from "../types/errors";
import { handleError, logError } from "./errorHandler";
const STORAGE_KEY = "twitch-panels";
const MAX_PANELS = 50; // Максимальное количество сохраненных панелей
export interface StorageResult {
success: boolean;
error?: string;
}
export class PanelStorage {
/**
* Сохраняет панель в localStorage
*/
savePanel(panel: Panel): StorageResult {
try {
const panels = this.getAllPanels();
// Проверяем, существует ли панель с таким ID
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, "Ошибка сохранения панели"),
};
}
}
/**
* Загружает все панели из localStorage
*/
getAllPanels(): Panel[] {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (!data) {
return [];
}
const panels: Panel[] = JSON.parse(data);
// Валидация загруженных панелей
return panels.filter((panel) => this.validatePanel(panel));
} catch (error) {
logError(error, "Failed to load panels");
return [];
}
}
/**
* Загружает панель по ID
*/
getPanelById(id: string): Panel | null {
try {
const panels = this.getAllPanels();
return panels.find((p) => p.id === id) || null;
} catch (error) {
logError(error, "Failed to get panel by id");
return null;
}
}
/**
* Удаляет панель по ID
*/
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" &&
Array.isArray(panel.texts) &&
typeof panel.height === "number" &&
panel.height > 0 &&
panel.height <= 1000 &&
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();