fix: resolve text preview error, add default panels

This commit is contained in:
2026-02-04 18:03:47 +05:00
parent d31e446fbd
commit bc7ed2498f
6 changed files with 105 additions and 63 deletions
+42 -15
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Panel } from "$lib/types/panel";
import type { Panel, TextItem } from "$lib/types/panel";
import { Image, Layer, Stage, Text } from "svelte-konva";
import { setCurrentStep } from "../../stores/uiStore";
import Button from "../ui/Button.svelte";
@@ -18,16 +18,28 @@
}
$effect(() => {
console.log(
"[PanelPreview] Effect triggered. Panel background:",
panel?.backgroundImage,
"Current image src:",
backgroundImage?.src,
);
if (panel?.backgroundImage && panel.backgroundImage !== backgroundImage?.src) {
console.log("[PanelPreview] Loading new background image:", panel.backgroundImage);
loadImage(panel.backgroundImage);
}
});
function loadImage(src: string) {
console.log("[PanelPreview] Starting to load image:", src);
const img = document.createElement("img");
img.onload = () => {
console.log("[PanelPreview] Image loaded successfully:", src);
backgroundImage = img;
};
img.onerror = (error) => {
console.error("[PanelPreview] Failed to load image:", src, error);
};
img.src = src;
}
@@ -39,25 +51,34 @@
function getTextPosition(textItem: TextItem) {
const panelWidth = 320;
const paddingX = textItem.paddingX || 0;
const paddingX = textItem.paddingX || 20; // Default padding
const verticalOffset = textItem.verticalOffset || 0;
const centerY = panel.height / 2 + verticalOffset;
const textWidth = 300; // Width of the text area
let position;
switch (textItem.textAlign) {
case "left":
return { x: paddingX, y: centerY };
position = { x: paddingX, y: centerY };
break;
case "right":
return { x: panelWidth - paddingX, y: centerY };
// For right alignment, position the right edge of text at panel edge minus padding
position = { x: panelWidth - textWidth - paddingX, y: centerY };
break;
case "center":
default:
return { x: panelWidth / 2, y: centerY };
// For center alignment, center the text area within the panel
position = { x: (panelWidth - textWidth) / 2, y: centerY };
break;
}
return position;
}
</script>
<div class="panel-preview">
<div class="preview-header">
<div class="panel-title">{panel.texts[0]?.text || "Без названия"}</div>
<div class="panel-title">{panel.text?.text || "Без названия"}</div>
<Button variant="primary" size="sm" onclick={handleDownload}>Скачать</Button>
</div>
<div class="preview-sections">
@@ -65,23 +86,29 @@
<div class="preview-section">
<div class="canvas-container">
<Stage width={320} height={panel.height}>
<!-- Background layer -->
<Layer>
{#if backgroundImage}
<Image image={backgroundImage} width={320} height={panel.height} />
{/if}
{#each panel.texts || [] as textItem (textItem.id)}
{@const textPosition = getTextPosition(textItem)}
</Layer>
<!-- Text layer (renders above background) -->
<Layer>
{#if panel.text}
{@const textPosition = getTextPosition(panel.text)}
<Text
text={textItem.text}
fontSize={textItem.fontSize}
fill={textItem.color}
fontFamily={textItem.fontFamily}
text={panel.text.text}
fontSize={panel.text.fontSize || 24}
fill={panel.text.color || "#ffffff"}
fontFamily={panel.text.fontFamily || "Arial"}
x={textPosition.x}
y={textPosition.y}
align={textItem.textAlign}
width={320 - textItem.paddingX * 2}
align={panel.text.textAlign || "center"}
width={300}
/>
{/each}
{/if}
</Layer>
</Stage>
</div>
+1 -2
View File
@@ -38,7 +38,7 @@ export class ExportService {
ctx.drawImage(bgImage, 0, 0, 320, panel.height);
// Рисуем текст
panel.texts.forEach((textItem) => {
const textItem = panel.text;
ctx.font = `${textItem.fontSize}px ${textItem.fontFamily}`;
ctx.fillStyle = textItem.color;
ctx.textAlign = "center";
@@ -49,7 +49,6 @@ export class ExportService {
const y = panel.height / 2 + textItem.verticalOffset;
ctx.fillText(textItem.text, x, y);
});
// Конвертируем в blob и сохраняем
canvas.toBlob((blob) => {
+1 -1
View File
@@ -14,7 +14,7 @@ export interface TextItem {
export interface Panel {
id: string;
backgroundImage: string;
texts: TextItem[];
text: TextItem;
height: number;
createdAt: Date;
updatedAt: Date;
+21 -1
View File
@@ -13,11 +13,31 @@
let backgroundImage = $state<string | undefined>(undefined);
onMount(async () => {
// Initialize with default texts for common Twitch panels
console.log("[INIT] Creating default texts...");
const defaultTexts = [
{ id: crypto.randomUUID(), text: "About" },
{ id: crypto.randomUUID(), text: "Links" },
];
texts = defaultTexts;
console.log("[INIT] Default texts created:", texts);
// Load background image
try {
backgroundImage = await imageService.loadDefaultBackground();
console.log("[INIT] Loading default background...");
const loadedBackground = await imageService.loadDefaultBackground();
console.log("[INIT] Background loaded:", loadedBackground);
backgroundImage = loadedBackground;
} catch (error) {
console.error("[INIT] Error loading background:", error);
exportService.setErrorMessage("Не удалось загрузить фоновое изображение по умолчанию");
}
// Create panels from texts (with or without background)
console.log("[INIT] Creating panels from texts...");
const initialPanels = panelService.updatePanelsFromTexts(texts, [], backgroundImage || "");
console.log("[INIT] Panels created:", initialPanels);
panels = initialPanels;
});
function handleImageUpload(image: string) {
+11 -7
View File
@@ -18,7 +18,7 @@ export class PanelService {
}
isDuplicateText(texts: Array<{ id: string; text: string }>, text: string, excludeId?: string): boolean {
return texts.some(t => t.id !== excludeId && t.text === text);
return texts.some((t) => t.id !== excludeId && t.text === text);
}
addText(texts: Array<{ id: string; text: string }>, text: string): Array<{ id: string; text: string }> {
@@ -28,20 +28,24 @@ export class PanelService {
return [...texts, { id: crypto.randomUUID(), text }];
}
updateText(texts: Array<{ id: string; text: string }>, id: string, newText: string): Array<{ id: string; text: string }> {
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);
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);
return texts.filter((t) => t.id !== id);
}
updatePanelsFromTexts(texts: Array<{ id: string; text: string }>, panels: Panel[], backgroundImage: string): Panel[] {
return texts.map(textItem => {
const existingPanel = panels.find(p => p.texts[0]?.text === textItem.text);
return texts.map((textItem) => {
const existingPanel = panels.find((p) => p.text.text === textItem.text);
if (existingPanel) {
return updatePanelText(existingPanel, textItem.text);
}
@@ -50,7 +54,7 @@ export class PanelService {
}
updatePanelsBackground(panels: Panel[], newBackground: string): Panel[] {
return panels.map(panel => ({
return panels.map((panel) => ({
...panel,
backgroundImage: newBackground,
updatedAt: new Date(),
+21 -29
View File
@@ -4,11 +4,24 @@ import { type Panel, type TextItem } from "../lib/types/panel";
export const panelStore: Writable<Panel | undefined> = writable(undefined);
// Panel creation should go through panelService.updatePanelsFromTexts()
// These functions are kept for backward compatibility but should be avoided
export const createEmptyPanel = (height: number = 100): Panel => {
const defaultText: TextItem = {
id: uuidv4(),
text: "",
fontSize: 18,
fontFamily: "Arial",
color: "#ffffff",
textAlign: "center",
paddingX: 10,
verticalOffset: 0,
};
return {
id: uuidv4(),
backgroundImage: "",
texts: [],
text: defaultText,
height,
createdAt: new Date(),
updatedAt: new Date(),
@@ -23,32 +36,16 @@ export const updatePanel = (panel: Panel, updates: Partial<Panel>): Panel => {
};
};
export const addTextToPanel = (panel: Panel, text: string): Panel => {
const newText: TextItem = {
id: uuidv4(),
text,
fontSize: 18,
fontFamily: "Arial",
color: "#ffffff",
textAlign: "center",
paddingX: 10,
verticalOffset: 0,
};
export const updatePanelText = (panel: Panel, text: string): Panel => {
return updatePanel(panel, {
texts: [...panel.texts, newText],
text: { ...panel.text, text },
});
};
export const updateTextInPanel = (panel: Panel, textId: string, updates: Partial<Panel["texts"][0]>): Panel => {
const updatedTexts = panel.texts.map((text) => (text.id === textId ? { ...text, ...updates } : text));
return updatePanel(panel, { texts: updatedTexts });
};
export const removeTextFromPanel = (panel: Panel, textId: string): Panel => {
const updatedTexts = panel.texts.filter((text) => text.id !== textId);
return updatePanel(panel, { texts: updatedTexts });
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 = 100): Panel => {
@@ -66,14 +63,9 @@ export const createPanelFromText = (backgroundImage: string, text: string, heigh
return {
id: uuidv4(),
backgroundImage,
texts: [newText],
text: newText,
height,
createdAt: new Date(),
updatedAt: new Date(),
};
};
export const updatePanelText = (panel: Panel, text: string): Panel => {
const updatedTexts = panel.texts.map((t) => ({ ...t, text }));
return updatePanel(panel, { texts: updatedTexts });
};