diff --git a/src/features/library/components/Library.svelte b/src/features/library/components/Library.svelte index ca5fd81..89fbdfe 100644 --- a/src/features/library/components/Library.svelte +++ b/src/features/library/components/Library.svelte @@ -71,8 +71,8 @@ let activeDragKind: "tab" | null = $state(null); let dragInsertIdx: number = $state(-1); - let dragTabId: number | null = $state(null); - let dragOverTabId: number | null = $state(null); + let dragTabId: string | null = $state(null); + let dragOverTabId: string | null = $state(null); const DT_TAB = "application/x-moku-tab"; const anims = $derived(store.settings.qolAnimations ?? true); @@ -95,7 +95,8 @@ const catIds = store.categories.filter(c => c.id !== 0).map(c => String(c.id)); const pinned = store.settings.libraryPinnedTabOrder ?? []; const known = new Set([...BUILTIN_TABS, ...catIds]); - const ordered = [...pinned.filter(id => known.has(id))]; + const eligible = pinned.filter(id => known.has(id)); + const ordered = [...eligible]; const inOrder = new Set(ordered); for (const id of [...BUILTIN_TABS, ...catIds]) { if (!inOrder.has(id)) ordered.push(id); @@ -517,45 +518,54 @@ }); } - function onTabDragStart(e: DragEvent, cat: Category) { - activeDragKind = "tab"; dragTabId = cat.id; + function onTabDragStart(e: DragEvent, id: string) { + activeDragKind = "tab"; dragTabId = id; e.dataTransfer!.effectAllowed = "move"; - e.dataTransfer!.setData(DT_TAB, String(cat.id)); - e.dataTransfer!.setData("text/plain", `tab:${cat.id}`); + e.dataTransfer!.setData(DT_TAB, id); + e.dataTransfer!.setData("text/plain", `tab:${id}`); } - function onTabDragOver(e: DragEvent, cat: Category, idx: number) { - if (activeDragKind !== "tab" || dragTabId === null || dragTabId === cat.id) return; + function onTabDragOver(e: DragEvent, id: string, idx: number) { + if (activeDragKind !== "tab" || dragTabId === null || dragTabId === id) return; e.preventDefault(); e.dataTransfer!.dropEffect = "move"; - dragOverTabId = cat.id; + dragOverTabId = id; const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); dragInsertIdx = e.clientX < rect.left + rect.width / 2 ? idx : idx + 1; } function onTabDragLeave() { dragOverTabId = null; } - async function onTabDrop(e: DragEvent, dropCat: Category) { + async function onTabDrop(e: DragEvent, dropId: string) { e.preventDefault(); dragOverTabId = null; const insertAt = dragInsertIdx; dragInsertIdx = -1; - if (activeDragKind !== "tab" || dragTabId === null || dragTabId === dropCat.id) { dragTabId = null; return; } - const dragId = dragTabId; dragTabId = null; activeDragKind = null; - const dragStrId = String(dragId); - const tabs = [...visibleTabIds]; + if (activeDragKind !== "tab" || dragTabId === null || dragTabId === dropId) { dragTabId = null; return; } + const dragStrId = dragTabId; dragTabId = null; activeDragKind = null; + + const tabs = [...allTabIds]; const fromIdx = tabs.indexOf(dragStrId); - if (fromIdx < 0) return; + const dropIdx = tabs.indexOf(dropId); + if (fromIdx < 0 || dropIdx < 0) return; + + const visibleDrop = visibleTabIds[insertAt] ?? null; + const destIdx = visibleDrop ? tabs.indexOf(visibleDrop) : tabs.length; + tabs.splice(fromIdx, 1); - const dest = Math.max(0, Math.min(insertAt > fromIdx ? insertAt - 1 : insertAt, tabs.length)); - tabs.splice(dest, 0, dragStrId); + const adjustedDest = Math.max(0, Math.min(destIdx > fromIdx ? destIdx - 1 : destIdx, tabs.length)); + tabs.splice(adjustedDest, 0, dragStrId); + updateSettings({ libraryPinnedTabOrder: tabs }); const catIds = tabs.filter(id => id !== "library" && id !== "downloaded"); const zeroCat = store.categories.filter(c => c.id === 0); const reordered = catIds.map((id, i) => { const c = store.categories.find(x => String(x.id) === id)!; return { ...c, order: i + 1 }; }); setCategories([...zeroCat, ...reordered]); - const serverPos = catIds.indexOf(dragStrId) + 1; - try { - await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: dragId, position: serverPos }); - } catch (err) { console.error("Tab reorder failed:", err); await reloadCategories(); } + const dragIsBuiltin = dragStrId === "library" || dragStrId === "downloaded"; + if (!dragIsBuiltin) { + const serverPos = catIds.indexOf(dragStrId) + 1; + try { + await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: Number(dragStrId), position: serverPos }); + } catch (err) { console.error("Tab reorder failed:", err); await reloadCategories(); } + } } function onTabDragEnd() { activeDragKind = null; dragTabId = null; dragOverTabId = null; dragInsertIdx = -1; } diff --git a/src/features/library/components/LibraryToolbar.svelte b/src/features/library/components/LibraryToolbar.svelte index a89ac2e..ef32428 100644 --- a/src/features/library/components/LibraryToolbar.svelte +++ b/src/features/library/components/LibraryToolbar.svelte @@ -24,8 +24,8 @@ search: string; activeDragKind: "tab" | null; dragInsertIdx: number; - dragTabId: number | null; - dragOverTabId: number | null; + dragTabId: string | null; + dragOverTabId: string | null; sortPanelOpen: boolean; filterPanelOpen: boolean; tabsEl: HTMLDivElement; @@ -39,10 +39,10 @@ onSortPanelToggle: () => void; onFilterPanelToggle: () => void; onOpenDownloadsFolder: () => void; - onTabDragStart: (e: DragEvent, cat: Category) => void; - onTabDragOver: (e: DragEvent, cat: Category, idx: number) => void; + onTabDragStart: (e: DragEvent, id: string) => void; + onTabDragOver: (e: DragEvent, id: string, idx: number) => void; onTabDragLeave: () => void; - onTabDrop: (e: DragEvent, cat: Category) => void; + onTabDrop: (e: DragEvent, id: string) => void; onTabDragEnd: () => void; } @@ -100,20 +100,23 @@ {#each visibleTabIds as id, idx} {@const cat = visibleCategories.find(c => String(c.id) === id)} {#if id === "library" || id === "downloaded" || cat} + {@const isBuiltin = id === "library" || id === "downloaded"} + {@const isCompleted = cat && id === String(completedCatId)} + {@const isDraggable = true} {#if activeDragKind === "tab" && dragInsertIdx === idx} {/if} - - - +
+ {#each orderedAllIds as id} + {@const isBuiltin = id === "library" || id === "downloaded"} + {@const isCompleted = id === completedId} + {@const cat = isBuiltin ? null : (store.categories.find(c => String(c.id) === id) ?? null)} + {@const hidden = isHidden(id)} -
- - Downloaded - built-in -
- - -
-
- {#if completedCat} -
- - {completedCat.name} - {completedCat.mangas?.nodes.length ?? 0} manga - built-in -
- - -
-
- {/if} - - - -
- {#each orderedCatIds.filter(id => id !== completedId) as id} - {@const cat = store.categories.find(c => String(c.id) === id) ?? null} - {@const hidden = isHidden(id)} - {#if cat} + {#if isBuiltin || cat}
onDragOver(e, cat.id)} - ondrop={(e) => onDrop(e, cat.id)} - ondragleave={() => { if (dragOverId === cat.id) { dragOverId = null; dropPosition = null; } }} + class:dragging={dragStrId === id} + class:drop-above={dragOverStrId === id && dragStrId !== id && dropPosition === "above"} + class:drop-below={dragOverStrId === id && dragStrId !== id && dropPosition === "below"} + draggable="true" + ondragstart={(e) => onDragStart(e, id)} + ondragover={(e) => onDragOver(e, id)} + ondragleave={() => { if (dragOverStrId === id) { dragOverStrId = null; dropPosition = null; } }} + ondrop={(e) => onDrop(e, id)} + ondragend={onDragEnd} > - {#if editingId === cat.id} - { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { editingId = null; } }} - onblur={commitEdit} use:focusInput /> - - {:else} -
onDragStart(e, cat.id)} - ondragend={onDragEnd}> - - - - - { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">{cat.name} -
- - {cat.mangas?.nodes.length ?? 0} manga + {#if isCompleted} + + + + + {cat?.name ?? "Completed"} + {cat?.mangas?.nodes.length ?? 0} manga + built-in
- - - - - +
+ + {:else if isBuiltin} + + {#if id === "library"}{:else}{/if} + + + {id === "library" ? "Saved" : "Downloaded"} + built-in +
+ + +
+ + {:else if cat} + {#if editingId === cat.id} + { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { editingId = null; } }} + onblur={commitEdit} use:focusInput /> + + {:else} +
onDragStart(e, id)} + ondragend={onDragEnd}> + + + + + { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">{cat.name} +
+ + {cat.mangas?.nodes.length ?? 0} manga + +
+ + + + + +
+ {/if} {/if}
{/if} @@ -314,28 +326,24 @@ .s-folder-row.drop-above::before { top: -1px; } .s-folder-row.drop-below::after { bottom: -1px; } - .s-folder-identity { - display: flex; - align-items: center; - gap: 6px; - color: var(--text-faint); - flex-shrink: 0; - overflow: hidden; - cursor: grab; - } - - .s-folder-row-static { - cursor: default; - } - .s-folder-icon-static { display: flex; align-items: center; flex-shrink: 0; - color: var(--text-faint); + color: var(--text-primary); width: 14px; } + .s-folder-identity { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-primary); + flex-shrink: 0; + overflow: hidden; + cursor: grab; + } + .s-folder-icon { display: grid; flex-shrink: 0; @@ -371,14 +379,6 @@ text-underline-offset: 3px; } - .s-folder-name-static { - cursor: default; - color: var(--text-secondary); - } - - .s-folder-name-static:hover { - text-decoration: none; - } .s-folder-actions { display: flex; @@ -400,12 +400,6 @@ margin-left: 6px; } - .s-folder-divider { - height: 1px; - background: var(--border-dim); - margin: 2px 0; - } - .s-btn-icon.active { color: var(--accent, #6c8ef5); }