fix: resolve text preview error, add default panels
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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 { Image, Layer, Stage, Text } from "svelte-konva";
|
||||||
import { setCurrentStep } from "../../stores/uiStore";
|
import { setCurrentStep } from "../../stores/uiStore";
|
||||||
import Button from "../ui/Button.svelte";
|
import Button from "../ui/Button.svelte";
|
||||||
@@ -18,16 +18,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
console.log(
|
||||||
|
"[PanelPreview] Effect triggered. Panel background:",
|
||||||
|
panel?.backgroundImage,
|
||||||
|
"Current image src:",
|
||||||
|
backgroundImage?.src,
|
||||||
|
);
|
||||||
if (panel?.backgroundImage && panel.backgroundImage !== backgroundImage?.src) {
|
if (panel?.backgroundImage && panel.backgroundImage !== backgroundImage?.src) {
|
||||||
|
console.log("[PanelPreview] Loading new background image:", panel.backgroundImage);
|
||||||
loadImage(panel.backgroundImage);
|
loadImage(panel.backgroundImage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadImage(src: string) {
|
function loadImage(src: string) {
|
||||||
|
console.log("[PanelPreview] Starting to load image:", src);
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
console.log("[PanelPreview] Image loaded successfully:", src);
|
||||||
backgroundImage = img;
|
backgroundImage = img;
|
||||||
};
|
};
|
||||||
|
img.onerror = (error) => {
|
||||||
|
console.error("[PanelPreview] Failed to load image:", src, error);
|
||||||
|
};
|
||||||
img.src = src;
|
img.src = src;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,25 +51,34 @@
|
|||||||
|
|
||||||
function getTextPosition(textItem: TextItem) {
|
function getTextPosition(textItem: TextItem) {
|
||||||
const panelWidth = 320;
|
const panelWidth = 320;
|
||||||
const paddingX = textItem.paddingX || 0;
|
const paddingX = textItem.paddingX || 20; // Default padding
|
||||||
const verticalOffset = textItem.verticalOffset || 0;
|
const verticalOffset = textItem.verticalOffset || 0;
|
||||||
const centerY = panel.height / 2 + verticalOffset;
|
const centerY = panel.height / 2 + verticalOffset;
|
||||||
|
const textWidth = 300; // Width of the text area
|
||||||
|
|
||||||
|
let position;
|
||||||
switch (textItem.textAlign) {
|
switch (textItem.textAlign) {
|
||||||
case "left":
|
case "left":
|
||||||
return { x: paddingX, y: centerY };
|
position = { x: paddingX, y: centerY };
|
||||||
|
break;
|
||||||
case "right":
|
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":
|
case "center":
|
||||||
default:
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="panel-preview">
|
<div class="panel-preview">
|
||||||
<div class="preview-header">
|
<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>
|
<Button variant="primary" size="sm" onclick={handleDownload}>Скачать</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-sections">
|
<div class="preview-sections">
|
||||||
@@ -65,23 +86,29 @@
|
|||||||
<div class="preview-section">
|
<div class="preview-section">
|
||||||
<div class="canvas-container">
|
<div class="canvas-container">
|
||||||
<Stage width={320} height={panel.height}>
|
<Stage width={320} height={panel.height}>
|
||||||
|
<!-- Background layer -->
|
||||||
<Layer>
|
<Layer>
|
||||||
{#if backgroundImage}
|
{#if backgroundImage}
|
||||||
<Image image={backgroundImage} width={320} height={panel.height} />
|
<Image image={backgroundImage} width={320} height={panel.height} />
|
||||||
{/if}
|
{/if}
|
||||||
{#each panel.texts || [] as textItem (textItem.id)}
|
</Layer>
|
||||||
{@const textPosition = getTextPosition(textItem)}
|
|
||||||
|
<!-- Text layer (renders above background) -->
|
||||||
|
<Layer>
|
||||||
|
{#if panel.text}
|
||||||
|
{@const textPosition = getTextPosition(panel.text)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
text={textItem.text}
|
text={panel.text.text}
|
||||||
fontSize={textItem.fontSize}
|
fontSize={panel.text.fontSize || 24}
|
||||||
fill={textItem.color}
|
fill={panel.text.color || "#ffffff"}
|
||||||
fontFamily={textItem.fontFamily}
|
fontFamily={panel.text.fontFamily || "Arial"}
|
||||||
x={textPosition.x}
|
x={textPosition.x}
|
||||||
y={textPosition.y}
|
y={textPosition.y}
|
||||||
align={textItem.textAlign}
|
align={panel.text.textAlign || "center"}
|
||||||
width={320 - textItem.paddingX * 2}
|
width={300}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/if}
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class ExportService {
|
|||||||
ctx.drawImage(bgImage, 0, 0, 320, panel.height);
|
ctx.drawImage(bgImage, 0, 0, 320, panel.height);
|
||||||
|
|
||||||
// Рисуем текст
|
// Рисуем текст
|
||||||
panel.texts.forEach((textItem) => {
|
const textItem = panel.text;
|
||||||
ctx.font = `${textItem.fontSize}px ${textItem.fontFamily}`;
|
ctx.font = `${textItem.fontSize}px ${textItem.fontFamily}`;
|
||||||
ctx.fillStyle = textItem.color;
|
ctx.fillStyle = textItem.color;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
@@ -49,7 +49,6 @@ export class ExportService {
|
|||||||
const y = panel.height / 2 + textItem.verticalOffset;
|
const y = panel.height / 2 + textItem.verticalOffset;
|
||||||
|
|
||||||
ctx.fillText(textItem.text, x, y);
|
ctx.fillText(textItem.text, x, y);
|
||||||
});
|
|
||||||
|
|
||||||
// Конвертируем в blob и сохраняем
|
// Конвертируем в blob и сохраняем
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface TextItem {
|
|||||||
export interface Panel {
|
export interface Panel {
|
||||||
id: string;
|
id: string;
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
texts: TextItem[];
|
text: TextItem;
|
||||||
height: number;
|
height: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
+21
-1
@@ -13,11 +13,31 @@
|
|||||||
let backgroundImage = $state<string | undefined>(undefined);
|
let backgroundImage = $state<string | undefined>(undefined);
|
||||||
|
|
||||||
onMount(async () => {
|
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 {
|
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) {
|
} catch (error) {
|
||||||
|
console.error("[INIT] Error loading background:", error);
|
||||||
exportService.setErrorMessage("Не удалось загрузить фоновое изображение по умолчанию");
|
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) {
|
function handleImageUpload(image: string) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class PanelService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isDuplicateText(texts: Array<{ id: string; text: string }>, text: string, excludeId?: string): boolean {
|
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 }> {
|
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 }];
|
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.validateText(newText)) return texts;
|
||||||
if (this.isDuplicateText(texts, newText, id)) 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 }> {
|
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[] {
|
updatePanelsFromTexts(texts: Array<{ id: string; text: string }>, panels: Panel[], backgroundImage: string): Panel[] {
|
||||||
return texts.map(textItem => {
|
return texts.map((textItem) => {
|
||||||
const existingPanel = panels.find(p => p.texts[0]?.text === textItem.text);
|
const existingPanel = panels.find((p) => p.text.text === textItem.text);
|
||||||
if (existingPanel) {
|
if (existingPanel) {
|
||||||
return updatePanelText(existingPanel, textItem.text);
|
return updatePanelText(existingPanel, textItem.text);
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,7 @@ export class PanelService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePanelsBackground(panels: Panel[], newBackground: string): Panel[] {
|
updatePanelsBackground(panels: Panel[], newBackground: string): Panel[] {
|
||||||
return panels.map(panel => ({
|
return panels.map((panel) => ({
|
||||||
...panel,
|
...panel,
|
||||||
backgroundImage: newBackground,
|
backgroundImage: newBackground,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|||||||
+21
-29
@@ -4,11 +4,24 @@ import { type Panel, type TextItem } from "../lib/types/panel";
|
|||||||
|
|
||||||
export const panelStore: Writable<Panel | undefined> = writable(undefined);
|
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 => {
|
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 {
|
return {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
backgroundImage: "",
|
backgroundImage: "",
|
||||||
texts: [],
|
text: defaultText,
|
||||||
height,
|
height,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: 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 => {
|
export const updatePanelText = (panel: Panel, text: string): Panel => {
|
||||||
const newText: TextItem = {
|
|
||||||
id: uuidv4(),
|
|
||||||
text,
|
|
||||||
fontSize: 18,
|
|
||||||
fontFamily: "Arial",
|
|
||||||
color: "#ffffff",
|
|
||||||
textAlign: "center",
|
|
||||||
paddingX: 10,
|
|
||||||
verticalOffset: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return updatePanel(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 => {
|
export const updateTextProperties = (panel: Panel, updates: Partial<TextItem>): Panel => {
|
||||||
const updatedTexts = panel.texts.map((text) => (text.id === textId ? { ...text, ...updates } : text));
|
return updatePanel(panel, {
|
||||||
|
text: { ...panel.text, ...updates },
|
||||||
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 createPanelFromText = (backgroundImage: string, text: string, height: number = 100): Panel => {
|
export const createPanelFromText = (backgroundImage: string, text: string, height: number = 100): Panel => {
|
||||||
@@ -66,14 +63,9 @@ export const createPanelFromText = (backgroundImage: string, text: string, heigh
|
|||||||
return {
|
return {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
texts: [newText],
|
text: newText,
|
||||||
height,
|
height,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: 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 });
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user