Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/lib/catalog/pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { VolumeData, Page } from '$lib/types';

/**
* Get the current working version of pages.
* Returns edited version if it exists, otherwise original.
*/
export function getCurrentPages(volumeData: VolumeData): Page[] {
return volumeData.edited_pages ?? volumeData.pages;
}

/**
* Check if volume has edits
*/
export function hasEdits(volumeData: VolumeData): boolean {
return volumeData.edited_pages !== undefined;
}

/**
* Get original pages (for reference in editor)
*/
export function getOriginalPages(volumeData: VolumeData): Page[] {
return volumeData.pages;
}
127 changes: 127 additions & 0 deletions src/lib/components/Editor/EditCanvas.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<script lang="ts">
import type { Page, Block } from '$lib/types';
import EditableBox from './EditableBox.svelte';
import { onMount } from 'svelte';

type ZoomMode = 'fit-screen' | 'fit-width' | 'original';

interface Props {
pageData: Page;
pageImage: string;
workingBlocks: Block[];
selectedIndex: number | null;
zoomMode: ZoomMode;
updateBlock: (index: number, block: Block) => void;
deleteBlock: (index: number) => void;
cloneBlock: (index: number) => number;
}

let {
pageData,
pageImage,
workingBlocks = $bindable(),
selectedIndex = $bindable(),
zoomMode,
updateBlock,
deleteBlock,
cloneBlock
}: Props = $props();

let containerRef: HTMLDivElement;
let scale = $state(1);

function handleSelect(index: number) {
selectedIndex = index;
}

function handleDeselect() {
selectedIndex = null;
}

function calculateZoom() {
if (!containerRef) return;

const container = containerRef;
const padding = 64; // 8 * 2 (p-8 on each side)
const containerWidth = container.clientWidth - padding;
const containerHeight = container.clientHeight - padding;
const pageWidth = pageData.img_width;
const pageHeight = pageData.img_height;

let newScale = 1;

switch (zoomMode) {
case 'fit-screen': {
const scaleX = containerWidth / pageWidth;
const scaleY = containerHeight / pageHeight;
newScale = Math.min(scaleX, scaleY);
break;
}
case 'fit-width': {
newScale = containerWidth / pageWidth;
break;
}
case 'original': {
newScale = 1;
break;
}
}

scale = newScale;
}

onMount(() => {
calculateZoom();
window.addEventListener('resize', calculateZoom);
return () => {
window.removeEventListener('resize', calculateZoom);
};
});

$effect(() => {
// Recalculate when zoom mode changes
zoomMode;
calculateZoom();
});
</script>

<div
bind:this={containerRef}
class="fixed top-16 right-0 bottom-0 left-0 flex items-center justify-center overflow-auto bg-gray-900 p-8"
>
<div
class="relative"
style:width="{pageData.img_width}px"
style:height="{pageData.img_height}px"
style:transform="scale({scale})"
style:transform-origin="center center"
>
<!-- Page Image -->
<img
src={pageImage}
alt="Manga page"
class="pointer-events-none absolute top-0 left-0 h-full w-full object-contain select-none"
draggable="false"
/>

<!-- Editable Text Boxes -->
{#each workingBlocks as block, index (index)}
<EditableBox
{block}
{index}
isSelected={selectedIndex === index}
imgWidth={pageData.img_width}
imgHeight={pageData.img_height}
onSelect={() => handleSelect(index)}
onUpdate={(updatedBlock) => updateBlock(index, updatedBlock)}
onDelete={() => deleteBlock(index)}
onClone={() => cloneBlock(index)}
bind:selectedIndex
/>
{/each}

<!-- Click outside to deselect -->
<button class="absolute inset-0 -z-10" onclick={handleDeselect} aria-label="Deselect box"
></button>
</div>
</div>
121 changes: 121 additions & 0 deletions src/lib/components/Editor/EditToolbar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script lang="ts">
import { Button, Toolbar, ToolbarGroup, Select } from 'flowbite-svelte';
import {
CaretLeftSolid,
CaretRightSolid,
FloppyDiskSolid,
DownloadSolid,
XSolid,
UndoOutline,
CirclePlusSolid
} from 'flowbite-svelte-icons';

type ZoomMode = 'fit-screen' | 'fit-width' | 'original';

interface Props {
pageIndex: number;
totalPages: number;
hasUnsavedChanges: boolean;
hasEdits: boolean;
zoomMode: ZoomMode;
onPrev: () => void;
onNext: () => void;
onSave: () => void;
onExport: () => void;
onExit: () => void;
onRevert: () => void;
onAddBox: () => void;
onZoomChange: (mode: ZoomMode) => void;
}

let {
pageIndex,
totalPages,
hasUnsavedChanges,
hasEdits,
zoomMode = $bindable(),
onPrev,
onNext,
onSave,
onExport,
onExit,
onRevert,
onAddBox,
onZoomChange
}: Props = $props();

const zoomOptions = [
{ value: 'fit-screen', name: 'Fit to Screen' },
{ value: 'fit-width', name: 'Fit to Width' },
{ value: 'original', name: 'Original Size' }
];

function handleZoomChange(event: Event) {
const target = event.target as HTMLSelectElement;
onZoomChange(target.value as ZoomMode);
}
</script>

<Toolbar
color="dark"
class="fixed top-0 right-0 left-0 z-50 flex items-center justify-between px-4 py-2"
>
<ToolbarGroup class="flex items-center gap-2">
<Button size="sm" color="alternative" onclick={onExit}>
<XSolid class="mr-2 h-4 w-4" />
Exit
</Button>

<div class="mx-4 font-medium text-white">
Page {pageIndex + 1} / {totalPages}
</div>

<Button size="sm" color="alternative" disabled={pageIndex <= 0} onclick={onPrev}>
<CaretLeftSolid class="mr-2 h-4 w-4" />
Previous
</Button>

<Button size="sm" color="alternative" disabled={pageIndex >= totalPages - 1} onclick={onNext}>
Next
<CaretRightSolid class="ml-2 h-4 w-4" />
</Button>

<div class="ml-4">
<Select
size="sm"
class="w-40"
value={zoomMode}
onchange={handleZoomChange}
items={zoomOptions}
/>
</div>
</ToolbarGroup>

<ToolbarGroup class="flex items-center gap-2">
{#if hasUnsavedChanges}
<span class="mr-2 text-sm text-yellow-400">Unsaved changes</span>
{/if}

<Button size="sm" color="green" onclick={onSave} disabled={!hasUnsavedChanges}>
<FloppyDiskSolid class="mr-2 h-4 w-4" />
Save
</Button>

<Button size="sm" color="purple" onclick={onAddBox}>
<CirclePlusSolid class="mr-2 h-4 w-4" />
Add Textbox
</Button>

{#if hasEdits}
<Button size="sm" color="red" onclick={onRevert}>
<UndoOutline class="mr-2 h-4 w-4" />
Revert to Original
</Button>
{/if}

<Button size="sm" color="blue" onclick={onExport}>
<DownloadSolid class="mr-2 h-4 w-4" />
Export .mokuro
</Button>
</ToolbarGroup>
</Toolbar>
Loading