diff --git a/ka-note/VERSION b/ka-note/VERSION index be2b557..48f713f 100644 --- a/ka-note/VERSION +++ b/ka-note/VERSION @@ -1 +1 @@ -1.2.28 \ No newline at end of file +1.2.31 \ No newline at end of file diff --git a/ka-note/client/src/lib/db/repositories.ts b/ka-note/client/src/lib/db/repositories.ts index 886a2d7..8e09bd6 100644 --- a/ka-note/client/src/lib/db/repositories.ts +++ b/ka-note/client/src/lib/db/repositories.ts @@ -1072,6 +1072,15 @@ export async function softDeleteAssetImage(id: string): Promise { } } +export async function reorderAssetImages(orderedIds: string[]): Promise { + await db.transaction('rw', db.assetImages, async () => { + for (let i = 0; i < orderedIds.length; i++) { + const ai = await db.assetImages.get(orderedIds[i]); + if (ai) await db.assetImages.update(orderedIds[i], { sortOrder: i, updatedAt: now(), version: ai.version + 1 }); + } + }); +} + // --- AssetPersons --- export async function addAssetPerson(assetId: string, personId: string): Promise { diff --git a/ka-note/client/src/lib/sync/syncService.ts b/ka-note/client/src/lib/sync/syncService.ts index e3132f9..ebdef12 100644 --- a/ka-note/client/src/lib/sync/syncService.ts +++ b/ka-note/client/src/lib/sync/syncService.ts @@ -353,13 +353,13 @@ async function doSync(since: Date | null): Promise { if (_syncInProgress) return; _syncInProgress = true; syncStatus.set('syncing'); + const syncStartedAt = new Date(); // record before push to avoid missing entities created during sync try { await pushAll(since); const sinceWithBuffer = since ? new Date(since.getTime() - 60_000) : null; - const serverTimestamp = await pullAndMerge(sinceWithBuffer); - const syncTime = new Date(serverTimestamp); - lastSyncAt.set(syncTime); - localStorage.setItem('lastSyncAt', syncTime.toISOString()); + await pullAndMerge(sinceWithBuffer); + lastSyncAt.set(syncStartedAt); + localStorage.setItem('lastSyncAt', syncStartedAt.toISOString()); syncStatus.set('idle'); } catch (err) { const msg = err instanceof Error ? err.message : String(err); diff --git a/ka-note/client/src/routes/inventory/[id]/+page.svelte b/ka-note/client/src/routes/inventory/[id]/+page.svelte index 0f767b0..c69f8f4 100644 --- a/ka-note/client/src/routes/inventory/[id]/+page.svelte +++ b/ka-note/client/src/routes/inventory/[id]/+page.svelte @@ -4,14 +4,14 @@ import { liveQuery } from 'dexie'; import { db } from '$lib/db/schema'; import { - updateAsset, softDeleteAsset, getAssetImages, addAssetImage, softDeleteAssetImage, + updateAsset, softDeleteAsset, addAssetImage, softDeleteAssetImage, reorderAssetImages, getAssetPersons, addAssetPerson, removeAssetPerson, getFamilyPersons, getRooms, } from '$lib/db/repositories'; import { getAccessToken } from '$lib/auth/authStore'; import { getImageUrl } from '$lib/db/imageStore'; import MarkdownEditor from '$lib/components/MarkdownEditor.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; - import { ChevronLeft, Package, Plus, Trash2, ScanSearch, Sparkles } from 'lucide-svelte'; + import { ChevronLeft, Package, Plus, Trash2, ScanSearch, Sparkles, Star } from 'lucide-svelte'; import DarkSelect from '$lib/components/DarkSelect.svelte'; import LabelConfirm from '$lib/components/LabelConfirm.svelte'; import { recognizePhoto, enrichAsset } from '$lib/services/visionService'; @@ -52,10 +52,18 @@ images = []; persons = []; db.assets.get(id).then(a => { asset = a; }); - getAssetImages(id).then(r => { images = r; loadImageUrls(r); }); getAssetPersons(id).then(r => { persons = r; }); getFamilyPersons().then(r => { familyPersons = r; }); getRooms().then(r => { rooms = r; }); + // Live subscription: update images whenever assetImages table changes + const sub = liveQuery(() => + db.assetImages.where('assetId').equals(id).filter(ai => !ai.deletedAt).sortBy('sortOrder') + ).subscribe(result => { images = result; }); + return () => sub.unsubscribe(); + }); + + $effect(() => { + loadImageUrls(images); }); $effect(() => { @@ -154,13 +162,9 @@ if (!res.ok) throw new Error('Upload failed'); const { id } = await res.json() as { id: string }; await addAssetImage(assetId, id); - // If no cover, set this as cover if (!asset?.coverImageId) { await save({ coverImageId: id }); } - const newImages = await getAssetImages(assetId); - images = newImages; - await loadImageUrls(newImages); } finally { uploading = false; } @@ -178,9 +182,6 @@ if (asset?.coverImageId === img.imageId) { await save({ coverImageId: null }); } - const newImages = await getAssetImages(assetId); - images = newImages; - await loadImageUrls(newImages); } async function setCover(img: AssetImage) { @@ -204,6 +205,36 @@ return meta?.abbreviation ?? meta?.fullName ?? p.name.replace(/^Person /, ''); } + let dragSrcId = $state(null); + let dragOverId = $state(null); + + function onDragStart(e: DragEvent, id: string) { + dragSrcId = id; + e.dataTransfer!.effectAllowed = 'move'; + } + + function onDragOver(e: DragEvent, id: string) { + e.preventDefault(); + dragOverId = id; + } + + function onDragEnd() { + dragSrcId = null; + dragOverId = null; + } + + async function onDrop(e: DragEvent, targetId: string) { + e.preventDefault(); + if (!dragSrcId || dragSrcId === targetId) { onDragEnd(); return; } + const ids = images.map(i => i.id); + const srcIdx = ids.indexOf(dragSrcId); + const tgtIdx = ids.indexOf(targetId); + ids.splice(srcIdx, 1); + ids.splice(tgtIdx, 0, dragSrcId); + onDragEnd(); + await reorderAssetImages(ids); + } + let notesValue = $state(''); let notesInit = $state(false); $effect(() => { @@ -245,10 +276,21 @@ {#if images.length > 0}
{#each images as img (img.id)} -
+ {@const isCover = img.imageId === asset.coverImageId} + {@const isDragOver = dragOverId === img.id && dragSrcId !== img.id} +
onDragStart(e, img.id)} + ondragover={(e) => onDragOver(e, img.id)} + ondrop={(e) => onDrop(e, img.id)} + ondragend={onDragEnd} + role="listitem" + > + + {#if isCover} + + + + {:else} + + {/if} +