fix: resolve on: events, svelte 5 updates
This commit is contained in:
@@ -1,11 +1,25 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Cropper from "cropperjs";
|
import { browser } from "$app/environment";
|
||||||
import type { ImageCropResult } from "../lib/types/panel";
|
import type { ImageCropResult } from "../lib/types/panel";
|
||||||
import { ImageService } from "../lib/services/imageService";
|
import { ImageService } from "../lib/services/imageService";
|
||||||
import { uiStore } from "../stores/uiStore";
|
import { uiStore } from "../stores/uiStore";
|
||||||
import "cropperjs/dist/cropper.css";
|
|
||||||
|
let Cropper: any = $state(undefined);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
|
const cropperModule = await import("cropperjs");
|
||||||
|
Cropper = cropperModule.default;
|
||||||
|
|
||||||
|
// Импортируем стили только на клиенте
|
||||||
|
await import("cropperjs/dist/cropper.css");
|
||||||
|
|
||||||
|
// Инициализируем cropper после загрузки
|
||||||
|
initializeCropper();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let { imageSrc, onCropComplete, onCancel }: {
|
let { imageSrc, onCropComplete, onCancel }: {
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
@@ -30,7 +44,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function initializeCropper() {
|
function initializeCropper() {
|
||||||
if (!imageElement) return;
|
if (!imageElement || !Cropper) return;
|
||||||
|
|
||||||
cropper = new Cropper(imageElement, {
|
cropper = new Cropper(imageElement, {
|
||||||
aspectRatio: NaN, // Allow any aspect ratio
|
aspectRatio: NaN, // Allow any aspect ratio
|
||||||
@@ -113,14 +127,14 @@
|
|||||||
<div class="cropper-actions">
|
<div class="cropper-actions">
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
on:click={handleCancel}
|
onclick={handleCancel}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
on:click={handleCrop}
|
onclick={handleCrop}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
{isProcessing ? "Обработка..." : "Применить"}
|
{isProcessing ? "Обработка..." : "Применить"}
|
||||||
|
|||||||
@@ -149,14 +149,14 @@
|
|||||||
<div class="uploaded-image-preview">
|
<div class="uploaded-image-preview">
|
||||||
<img src={uploadedImage} alt="Uploaded preview" />
|
<img src={uploadedImage} alt="Uploaded preview" />
|
||||||
<div class="preview-actions">
|
<div class="preview-actions">
|
||||||
<button class="btn btn-secondary" on:click={resetUpload}> Загрузить другое </button>
|
<button class="btn btn-secondary" onclick={resetUpload}> Загрузить другое </button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
bind:this={dropZone}
|
bind:this={dropZone}
|
||||||
class="drop-zone {isDragOver ? 'drag-over' : ''} {uiError ? 'error' : ''}"
|
class="drop-zone {isDragOver ? 'drag-over' : ''} {uiError ? 'error' : ''}"
|
||||||
on:click={triggerFileInput}
|
onclick={triggerFileInput}
|
||||||
>
|
>
|
||||||
<div class="drop-zone-content">
|
<div class="drop-zone-content">
|
||||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -169,15 +169,15 @@
|
|||||||
<p>Перетащите файл сюда, нажмите для выбора, или вставьте через Ctrl+V</p>
|
<p>Перетащите файл сюда, нажмите для выбора, или вставьте через Ctrl+V</p>
|
||||||
|
|
||||||
<div class="upload-methods">
|
<div class="upload-methods">
|
||||||
<button class="btn btn-primary" on:click={triggerFileInput}> Выбрать файл </button>
|
<button class="btn btn-primary" onclick={triggerFileInput}> Выбрать файл </button>
|
||||||
|
|
||||||
<button class="btn btn-secondary" on:click={() => (showUrlInput = !showUrlInput)}>
|
<button class="btn btn-secondary" onclick={() => (showUrlInput = !showUrlInput)}>
|
||||||
{showUrlInput ? "Скрыть" : "По URL"}
|
{showUrlInput ? "Скрыть" : "По URL"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showUrlInput}
|
{#if showUrlInput}
|
||||||
<form bind:this={urlForm} on:submit={handleUrlSubmit} class="url-input-form">
|
<form bind:this={urlForm} onsubmit={handleUrlSubmit} class="url-input-form">
|
||||||
<input
|
<input
|
||||||
bind:this={urlInput}
|
bind:this={urlInput}
|
||||||
type="url"
|
type="url"
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
type="file"
|
type="file"
|
||||||
accept="image/jpeg,image/jpg,image/png,image/webp,image/gif"
|
accept="image/jpeg,image/jpg,image/png,image/webp,image/gif"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
on:change={(e) => {
|
onchange={(e) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) handleFileUpload(file);
|
if (file) handleFileUpload(file);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<div class="panel-list">
|
<div class="panel-list">
|
||||||
<div class="panel-list-header">
|
<div class="panel-list-header">
|
||||||
<h2>Сохраненные панели</h2>
|
<h2>Сохраненные панели</h2>
|
||||||
<button class="btn btn-refresh" on:click={loadPanels} aria-label="Обновить список">
|
<button class="btn btn-refresh" onclick={loadPanels} aria-label="Обновить список">
|
||||||
↻
|
↻
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
{#each panels as panel (panel.id)}
|
{#each panels as panel (panel.id)}
|
||||||
<div
|
<div
|
||||||
class="panel-card {selectedPanelId === panel.id ? 'selected' : ''}"
|
class="panel-card {selectedPanelId === panel.id ? 'selected' : ''}"
|
||||||
on:click={() => handlePanelSelect(panel)}
|
onclick={() => handlePanelSelect(panel)}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-icon btn-delete"
|
class="btn-icon btn-delete"
|
||||||
on:click|stopPropagation={() => handlePanelDelete(panel.id)}
|
onclick={(e) => { e.stopPropagation(); handlePanelDelete(panel.id); }}
|
||||||
aria-label="Удалить панель"
|
aria-label="Удалить панель"
|
||||||
>
|
>
|
||||||
🗑️
|
🗑️
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
import { panelStore } from "../stores/panelStore";
|
import { panelStore } from "../stores/panelStore";
|
||||||
import type { Panel } from "../lib/types/panel";
|
import type { Panel } from "../lib/types/panel";
|
||||||
import type { PageProps } from "./$types";
|
import type { PageProps } from "./$types";
|
||||||
@@ -25,11 +26,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
const svelteKonva = await import("svelte-konva");
|
const svelteKonva = await import("svelte-konva");
|
||||||
Stage = svelteKonva.Stage;
|
Stage = svelteKonva.Stage;
|
||||||
Layer = svelteKonva.Layer;
|
Layer = svelteKonva.Layer;
|
||||||
Image = svelteKonva.Image;
|
Image = svelteKonva.Image;
|
||||||
Text = svelteKonva.Text;
|
Text = svelteKonva.Text;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadImage(src: string) {
|
function loadImage(src: string) {
|
||||||
@@ -51,7 +54,7 @@
|
|||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<h2>Предпросмотр панели</h2>
|
<h2>Предпросмотр панели</h2>
|
||||||
{#if currentPanel}
|
{#if currentPanel}
|
||||||
<button class="btn btn-primary" on:click={handleDownload}>
|
<button class="btn btn-primary" onclick={handleDownload}>
|
||||||
Скачать
|
Скачать
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
class="text-input"
|
class="text-input"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-primary" on:click={handleAddText}>
|
<button class="btn btn-primary" onclick={handleAddText}>
|
||||||
Добавить
|
Добавить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,13 +108,13 @@
|
|||||||
{#each currentPanel.texts as textItem (textItem.id)}
|
{#each currentPanel.texts as textItem (textItem.id)}
|
||||||
<div
|
<div
|
||||||
class="text-item {selectedTextId === textItem.id ? 'selected' : ''}"
|
class="text-item {selectedTextId === textItem.id ? 'selected' : ''}"
|
||||||
on:click={() => handleSelectText(textItem.id)}
|
onclick={() => handleSelectText(textItem.id)}
|
||||||
>
|
>
|
||||||
<div class="text-preview">
|
<div class="text-preview">
|
||||||
<span class="text-content">{textItem.text}</span>
|
<span class="text-content">{textItem.text}</span>
|
||||||
<button
|
<button
|
||||||
class="btn-icon btn-delete"
|
class="btn-icon btn-delete"
|
||||||
on:click|stopPropagation={() => handleRemoveText(textItem.id)}
|
onclick={(e) => { e.stopPropagation(); handleRemoveText(textItem.id); }}
|
||||||
aria-label="Удалить текст"
|
aria-label="Удалить текст"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
max="72"
|
max="72"
|
||||||
step="1"
|
step="1"
|
||||||
value={textItem.fontSize}
|
value={textItem.fontSize}
|
||||||
on:input={(e) => handleUpdateText(textItem.id, {
|
oninput={(e) => handleUpdateText(textItem.id, {
|
||||||
fontSize: parseInt(e.target.value)
|
fontSize: parseInt(e.target.value)
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={textItem.color}
|
value={textItem.color}
|
||||||
on:input={(e) => handleUpdateText(textItem.id, {
|
oninput={(e) => handleUpdateText(textItem.id, {
|
||||||
color: e.target.value
|
color: e.target.value
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
max="320"
|
max="320"
|
||||||
step="1"
|
step="1"
|
||||||
value={textItem.x}
|
value={textItem.x}
|
||||||
on:input={(e) => handleUpdateText(textItem.id, {
|
oninput={(e) => handleUpdateText(textItem.id, {
|
||||||
x: parseInt(e.target.value)
|
x: parseInt(e.target.value)
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
max={currentPanel.height}
|
max={currentPanel.height}
|
||||||
step="1"
|
step="1"
|
||||||
value={textItem.y}
|
value={textItem.y}
|
||||||
on:input={(e) => handleUpdateText(textItem.id, {
|
oninput={(e) => handleUpdateText(textItem.id, {
|
||||||
y: parseInt(e.target.value)
|
y: parseInt(e.target.value)
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const ssr = true;
|
export const ssr = false;
|
||||||
export const prerender = true;
|
export const prerender = false;
|
||||||
|
|||||||
+36
-11
@@ -6,13 +6,26 @@
|
|||||||
import { uiStore } from "../stores/uiStore";
|
import { uiStore } from "../stores/uiStore";
|
||||||
import { panelStorage } from "../lib/utils/panelStorage";
|
import { panelStorage } from "../lib/utils/panelStorage";
|
||||||
import { exportService } from "../lib/services/exportService";
|
import { exportService } from "../lib/services/exportService";
|
||||||
import ImageUpload from "../components/ImageUpload.svelte";
|
|
||||||
import ImageCropper from "../components/ImageCropper.svelte";
|
|
||||||
import TextManager from "../components/TextManager.svelte";
|
|
||||||
import PanelPreview from "../components/PanelPreview.svelte";
|
|
||||||
import PanelList from "../components/PanelList.svelte";
|
|
||||||
import type { Panel } from "../lib/types/panel";
|
import type { Panel } from "../lib/types/panel";
|
||||||
|
|
||||||
|
let ImageUpload: any = $state(undefined);
|
||||||
|
let ImageCropper: any = $state(undefined);
|
||||||
|
let TextManager: any = $state(undefined);
|
||||||
|
let PanelPreview: any = $state(undefined);
|
||||||
|
let PanelList: any = $state(undefined);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
|
[ImageUpload, ImageCropper, TextManager, PanelPreview, PanelList] = await Promise.all([
|
||||||
|
import("../components/ImageUpload.svelte"),
|
||||||
|
import("../components/ImageCropper.svelte"),
|
||||||
|
import("../components/TextManager.svelte"),
|
||||||
|
import("../components/PanelPreview.svelte"),
|
||||||
|
import("../components/PanelList.svelte")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let uploadedImage = $state<string | null>(null);
|
let uploadedImage = $state<string | null>(null);
|
||||||
let croppedImage = $state<string | null>(null);
|
let croppedImage = $state<string | null>(null);
|
||||||
let currentPanel = $state<Panel | null>(null);
|
let currentPanel = $state<Panel | null>(null);
|
||||||
@@ -115,7 +128,7 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div class="app-header">
|
<div class="app-header">
|
||||||
<h1>Twitch Panels Creator</h1>
|
<h1>Twitch Panels Creator</h1>
|
||||||
<button class="btn btn-primary" on:click={handleNewPanel}>Новая панель</button>
|
<button class="btn btn-primary" onclick={handleNewPanel}>Новая панель</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
@@ -127,25 +140,37 @@
|
|||||||
<div class="app-content">
|
<div class="app-content">
|
||||||
<div class="main-section">
|
<div class="main-section">
|
||||||
{#if $uiStore.currentStep === "upload"}
|
{#if $uiStore.currentStep === "upload"}
|
||||||
<ImageUpload onImageSelect={handleImageUpload} />
|
{#if ImageUpload}
|
||||||
|
<svelte:component this={ImageUpload.default} onImageSelect={handleImageUpload} />
|
||||||
|
{/if}
|
||||||
{:else if $uiStore.currentStep === "crop" && uploadedImage}
|
{:else if $uiStore.currentStep === "crop" && uploadedImage}
|
||||||
<ImageCropper
|
{#if ImageCropper}
|
||||||
|
<svelte:component
|
||||||
|
this={ImageCropper.default}
|
||||||
imageSrc={uploadedImage}
|
imageSrc={uploadedImage}
|
||||||
onCropComplete={handleCropComplete}
|
onCropComplete={handleCropComplete}
|
||||||
onCancel={handleCropCancel}
|
onCancel={handleCropCancel}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
{:else if $uiStore.currentStep === "text"}
|
{:else if $uiStore.currentStep === "text"}
|
||||||
<TextManager onTextUpdate={handleTextUpdate} />
|
{#if TextManager}
|
||||||
|
<svelte:component this={TextManager.default} onTextUpdate={handleTextUpdate} />
|
||||||
|
{/if}
|
||||||
{:else if $uiStore.currentStep === "preview"}
|
{:else if $uiStore.currentStep === "preview"}
|
||||||
<PanelPreview onDownload={handleDownload} />
|
{#if PanelPreview}
|
||||||
|
<svelte:component this={PanelPreview.default} onDownload={handleDownload} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<PanelList
|
{#if PanelList}
|
||||||
|
<svelte:component
|
||||||
|
this={PanelList.default}
|
||||||
onPanelSelect={handlePanelSelect}
|
onPanelSelect={handlePanelSelect}
|
||||||
onPanelDelete={handlePanelDelete}
|
onPanelDelete={handlePanelDelete}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user