mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: FolderSettings Revamp & Folders (#63)
This commit is contained in:
@@ -19,10 +19,6 @@
|
|||||||
visibleCategories: Category[];
|
visibleCategories: Category[];
|
||||||
counts: Record<string, number>;
|
counts: Record<string, number>;
|
||||||
search: string;
|
search: string;
|
||||||
refreshing: boolean;
|
|
||||||
refreshProgress: { finished: number; total: number };
|
|
||||||
refreshDone: boolean;
|
|
||||||
refreshingCatId: number | null;
|
|
||||||
activeDragKind: "tab" | null;
|
activeDragKind: "tab" | null;
|
||||||
dragInsertIdx: number;
|
dragInsertIdx: number;
|
||||||
dragTabId: number | null;
|
dragTabId: number | null;
|
||||||
@@ -39,9 +35,6 @@
|
|||||||
onFiltersClear: () => void;
|
onFiltersClear: () => void;
|
||||||
onSortPanelToggle: () => void;
|
onSortPanelToggle: () => void;
|
||||||
onFilterPanelToggle: () => void;
|
onFilterPanelToggle: () => void;
|
||||||
onRefresh: () => void;
|
|
||||||
onCancelRefresh: () => void;
|
|
||||||
onRefreshCategory: (catId: number) => void;
|
|
||||||
onOpenDownloadsFolder: () => void;
|
onOpenDownloadsFolder: () => void;
|
||||||
onTabDragStart: (e: DragEvent, cat: Category) => void;
|
onTabDragStart: (e: DragEvent, cat: Category) => void;
|
||||||
onTabDragOver: (e: DragEvent, cat: Category, idx: number) => void;
|
onTabDragOver: (e: DragEvent, cat: Category, idx: number) => void;
|
||||||
@@ -53,12 +46,12 @@
|
|||||||
let {
|
let {
|
||||||
tab, tabSortMode, tabSortDir, tabStatus, tabFilters, hasActiveFilters,
|
tab, tabSortMode, tabSortDir, tabStatus, tabFilters, hasActiveFilters,
|
||||||
anims, tabIndicator, visibleCategories, counts, search, refreshing,
|
anims, tabIndicator, visibleCategories, counts, search, refreshing,
|
||||||
refreshProgress, refreshDone, refreshingCatId, activeDragKind, dragInsertIdx,
|
refreshProgress, refreshDone, activeDragKind, dragInsertIdx,
|
||||||
dragTabId, dragOverTabId, sortPanelOpen, filterPanelOpen,
|
dragTabId, dragOverTabId, sortPanelOpen, filterPanelOpen,
|
||||||
tabsEl = $bindable(),
|
tabsEl = $bindable(),
|
||||||
onSearchChange, onTabChange, onSortChange, onSortDirToggle, onStatusChange,
|
onSearchChange, onTabChange, onSortChange, onSortDirToggle, onStatusChange,
|
||||||
onFilterToggle, onFiltersClear, onSortPanelToggle, onFilterPanelToggle,
|
onFilterToggle, onFiltersClear, onSortPanelToggle, onFilterPanelToggle,
|
||||||
onRefresh, onCancelRefresh, onRefreshCategory, onOpenDownloadsFolder,
|
onRefresh, onOpenDownloadsFolder,
|
||||||
onTabDragStart, onTabDragOver, onTabDragLeave, onTabDrop, onTabDragEnd,
|
onTabDragStart, onTabDragOver, onTabDragLeave, onTabDrop, onTabDragEnd,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -118,20 +111,6 @@
|
|||||||
<Folder size={11} weight="bold" />
|
<Folder size={11} weight="bold" />
|
||||||
{cat.name}
|
{cat.name}
|
||||||
<span class="tab-count">{counts[String(cat.id)] ?? 0}</span>
|
<span class="tab-count">{counts[String(cat.id)] ?? 0}</span>
|
||||||
{#if tab === String(cat.id) && !refreshing}
|
|
||||||
<span
|
|
||||||
class="tab-refresh"
|
|
||||||
role="button"
|
|
||||||
tabindex="-1"
|
|
||||||
title="Refresh {cat.name}"
|
|
||||||
aria-label="Refresh {cat.name}"
|
|
||||||
class:tab-refresh-spinning={refreshingCatId === cat.id}
|
|
||||||
onclick={(e) => { e.stopPropagation(); onRefreshCategory(cat.id); }}
|
|
||||||
onkeydown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onRefreshCategory(cat.id); } }}
|
|
||||||
>
|
|
||||||
<ArrowsClockwise size={10} weight="bold" class={refreshingCatId === cat.id ? "anim-spin" : ""} />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{#if dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleCategories.length - 1}
|
{#if dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleCategories.length - 1}
|
||||||
<div class="tab-insert-bar" aria-hidden="true"></div>
|
<div class="tab-insert-bar" aria-hidden="true"></div>
|
||||||
@@ -150,10 +129,10 @@
|
|||||||
{#if refreshing}
|
{#if refreshing}
|
||||||
<button
|
<button
|
||||||
class="icon-btn refresh-btn icon-btn-active"
|
class="icon-btn refresh-btn icon-btn-active"
|
||||||
title="Cancel update"
|
title={`Checking… ${refreshProgress.finished}/${refreshProgress.total}`}
|
||||||
onclick={onCancelRefresh}
|
onclick={onRefresh}
|
||||||
>
|
>
|
||||||
<X size={15} weight="bold" />
|
<ArrowsClockwise size={15} weight="bold" class="anim-spin" />
|
||||||
{#if refreshProgress.total > 0}
|
{#if refreshProgress.total > 0}
|
||||||
<span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span>
|
<span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -241,10 +220,6 @@
|
|||||||
.tab-dragging { opacity: 0.4; cursor: grabbing; }
|
.tab-dragging { opacity: 0.4; cursor: grabbing; }
|
||||||
.tab-insert-bar { width: 2px; height: 22px; background: var(--accent); border-radius: 2px; flex-shrink: 0; box-shadow: 0 0 6px var(--accent); pointer-events: none; }
|
.tab-insert-bar { width: 2px; height: 22px; background: var(--accent); border-radius: 2px; flex-shrink: 0; box-shadow: 0 0 6px var(--accent); pointer-events: none; }
|
||||||
.tab-count { font-size: var(--text-2xs); opacity: 0.6; }
|
.tab-count { font-size: var(--text-2xs); opacity: 0.6; }
|
||||||
.tab-refresh { display: flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 2px; opacity: 0; color: var(--accent-fg); cursor: pointer; transition: opacity var(--t-base), background var(--t-base); flex-shrink: 0; }
|
|
||||||
.tab.active:hover .tab-refresh { opacity: 0.6; }
|
|
||||||
.tab.active:hover .tab-refresh:hover { opacity: 1; background: var(--accent-dim); }
|
|
||||||
.tab-refresh-spinning { opacity: 1 !important; }
|
|
||||||
.search-wrap { position: relative; display: flex; align-items: center; }
|
.search-wrap { position: relative; display: flex; align-items: center; }
|
||||||
.search-wrap :global(.search-icon) { position: absolute; left: 10px; color: var(--text-faint); pointer-events: none; }
|
.search-wrap :global(.search-icon) { position: absolute; left: 10px; color: var(--text-faint); pointer-events: none; }
|
||||||
.search { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px 5px 28px; color: var(--text-primary); font-size: var(--text-sm); width: 180px; outline: none; transition: border-color var(--t-base); }
|
.search { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px 5px 28px; color: var(--text-primary); font-size: var(--text-sm); width: 180px; outline: none; transition: border-color var(--t-base); }
|
||||||
@@ -253,7 +228,9 @@
|
|||||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
.icon-btn { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||||
.icon-btn:hover { color: var(--text-primary); border-color: var(--border-strong); }
|
.icon-btn:hover { color: var(--text-primary); border-color: var(--border-strong); }
|
||||||
.icon-btn-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
.icon-btn-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||||
|
|
||||||
.refresh-btn { gap: var(--sp-1); width: auto; padding: 0 8px; }
|
.refresh-btn { gap: var(--sp-1); width: auto; padding: 0 8px; }
|
||||||
|
.refresh-btn:disabled { cursor: default; }
|
||||||
.refresh-progress { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); }
|
.refresh-progress { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); }
|
||||||
.refresh-btn-done { color: var(--color-success, #5cae6e) !important; border-color: color-mix(in srgb, var(--color-success, #5cae6e) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success, #5cae6e) 10%, transparent) !important; }
|
.refresh-btn-done { color: var(--color-success, #5cae6e) !important; border-color: color-mix(in srgb, var(--color-success, #5cae6e) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success, #5cae6e) 10%, transparent) !important; }
|
||||||
.sort-panel-wrap { position: relative; }
|
.sort-panel-wrap { position: relative; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FolderSimple, Plus, Pencil, Trash, Star, Eye, EyeSlash, ArrowsClockwise, DownloadSimple } from "phosphor-svelte";
|
import { FolderSimple, Plus, Trash, Star, Eye, EyeSlash, ArrowsClockwise, ArrowsCounterClockwise, DownloadSimple, DotsSixVertical } from "phosphor-svelte";
|
||||||
import { gql } from "@api/client";
|
import { gql } from "@api/client";
|
||||||
import { GET_CATEGORIES } from "@api/queries/manga";
|
import { GET_CATEGORIES } from "@api/queries/manga";
|
||||||
import { CREATE_CATEGORY, UPDATE_CATEGORY, UPDATE_CATEGORIES, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER } from "@api/mutations/manga";
|
import { CREATE_CATEGORY, UPDATE_CATEGORY, UPDATE_CATEGORIES, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER } from "@api/mutations/manga";
|
||||||
@@ -12,13 +12,17 @@
|
|||||||
let editingId = $state<number | null>(null);
|
let editingId = $state<number | null>(null);
|
||||||
let editingName = $state("");
|
let editingName = $state("");
|
||||||
|
|
||||||
|
let dragId = $state<number | null>(null);
|
||||||
|
let dragOverId = $state<number | null>(null);
|
||||||
|
let dropPosition = $state<"above" | "below" | null>(null);
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
catsLoading = true; catsError = null;
|
catsLoading = true; catsError = null;
|
||||||
try {
|
try {
|
||||||
const res = await gql<{ categories: { nodes: Category[] } }>(GET_CATEGORIES);
|
const res = await gql<{ categories: { nodes: Category[] } }>(GET_CATEGORIES);
|
||||||
const zeroCat = store.categories.filter(c => c.id === 0);
|
const zeroCat = store.categories.filter(c => c.id === 0);
|
||||||
const fresh = res.categories.nodes.filter(c => c.id !== 0);
|
const fresh = res.categories.nodes.filter(c => c.id !== 0);
|
||||||
const merged = fresh.map(f => {
|
const merged = fresh.map(f => {
|
||||||
const existing = store.categories.find(c => c.id === f.id);
|
const existing = store.categories.find(c => c.id === f.id);
|
||||||
return existing ? { ...existing, ...f } : f;
|
return existing ? { ...existing, ...f } : f;
|
||||||
});
|
});
|
||||||
@@ -63,26 +67,25 @@
|
|||||||
const next = !cat[flag];
|
const next = !cat[flag];
|
||||||
setCategories(store.categories.map(c => c.id === id ? { ...c, [flag]: next } : c));
|
setCategories(store.categories.map(c => c.id === id ? { ...c, [flag]: next } : c));
|
||||||
try {
|
try {
|
||||||
await gql(UPDATE_CATEGORIES, { ids: [id], patch: { [flag]: next } });
|
await gql(UPDATE_CATEGORIES, { ids: [id], patch: { [flag]: next ? "INCLUDE" : "EXCLUDE" } });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setCategories(store.categories.map(c => c.id === id ? { ...c, [flag]: !next } : c));
|
setCategories(store.categories.map(c => c.id === id ? { ...c, [flag]: !next } : c));
|
||||||
catsError = e?.message ?? "Failed to update folder";
|
catsError = e?.message ?? "Failed to update folder";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveCategory(id: number, direction: -1 | 1) {
|
async function applyReorder(fromId: number, toId: number) {
|
||||||
const zeroCat = store.categories.filter(c => c.id === 0);
|
const zeroCat = store.categories.filter(c => c.id === 0);
|
||||||
const sortable = store.categories.filter(c => c.id !== 0).sort((a, b) => a.order - b.order);
|
const sortable = store.categories.filter(c => c.id !== 0).sort((a, b) => a.order - b.order);
|
||||||
const idx = sortable.findIndex(c => c.id === id);
|
const fromIdx = sortable.findIndex(c => c.id === fromId);
|
||||||
if (idx < 0) return;
|
const toIdx = sortable.findIndex(c => c.id === toId);
|
||||||
const newPos = idx + 1 + direction;
|
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return;
|
||||||
if (newPos < 1 || newPos > sortable.length) return;
|
|
||||||
const reordered = [...sortable];
|
const reordered = [...sortable];
|
||||||
const [moved] = reordered.splice(idx, 1);
|
const [moved] = reordered.splice(fromIdx, 1);
|
||||||
reordered.splice(idx + direction, 0, moved);
|
reordered.splice(toIdx, 0, moved);
|
||||||
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
|
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
|
||||||
try {
|
try {
|
||||||
const res = await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id, position: newPos });
|
const res = await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: fromId, position: toIdx + 1 });
|
||||||
const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
|
const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
|
||||||
setCategories([
|
setCategories([
|
||||||
...zeroCat,
|
...zeroCat,
|
||||||
@@ -97,6 +100,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDragStart(e: DragEvent, id: number) {
|
||||||
|
dragId = id;
|
||||||
|
if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(id)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(e: DragEvent, id: number) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||||
|
if (dragId === id) return;
|
||||||
|
dragOverId = id;
|
||||||
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
dropPosition = e.clientY < rect.top + rect.height / 2 ? "above" : "below";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e: DragEvent, id: number) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (dragId !== null && dragId !== id) applyReorder(dragId, id);
|
||||||
|
dragId = null; dragOverId = null; dropPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() { dragId = null; dragOverId = null; dropPosition = null; }
|
||||||
|
|
||||||
function focusInput(node: HTMLElement) { node.focus(); }
|
function focusInput(node: HTMLElement) { node.focus(); }
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -105,7 +130,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="s-panel">
|
<div class="s-panel">
|
||||||
|
|
||||||
<div class="s-section">
|
<div class="s-section">
|
||||||
<p class="s-section-title">Manage Folders</p>
|
<p class="s-section-title">Manage Folders</p>
|
||||||
<div class="s-section-body">
|
<div class="s-section-body">
|
||||||
@@ -135,51 +159,196 @@
|
|||||||
if (b.id === defaultId) return 1;
|
if (b.id === defaultId) return 1;
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
})}
|
})}
|
||||||
{#each displayCats as cat, i}
|
<div class="s-folder-list" class:is-dragging={dragId !== null}>
|
||||||
<div class="s-folder-row">
|
{#each displayCats as cat}
|
||||||
{#if editingId === cat.id}
|
<div
|
||||||
<input class="s-input full" bind:value={editingName}
|
class="s-folder-row"
|
||||||
onkeydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") editingId = null; }}
|
class:dragging={dragId === cat.id}
|
||||||
onblur={commitEdit} use:focusInput />
|
class:drop-above={dragOverId === cat.id && dragId !== cat.id && dropPosition === "above"}
|
||||||
<button class="s-btn-icon" onclick={commitEdit} title="Save">✓</button>
|
class:drop-below={dragOverId === cat.id && dragId !== cat.id && dropPosition === "below"}
|
||||||
{:else}
|
ondragover={(e) => onDragOver(e, cat.id)}
|
||||||
<FolderSimple size={14} weight="light" style="color:var(--text-faint);flex-shrink:0" />
|
ondrop={(e) => onDrop(e, cat.id)}
|
||||||
<span class="s-folder-name">{cat.name}</span>
|
ondragleave={() => { if (dragOverId === cat.id) { dragOverId = null; dropPosition = null; } }}
|
||||||
<span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span>
|
>
|
||||||
<button class="s-btn-icon"
|
{#if editingId === cat.id}
|
||||||
class:accent={(store.settings.defaultLibraryCategoryId ?? null) === cat.id}
|
<input class="s-input full" bind:value={editingName}
|
||||||
onclick={() => updateSettings({ defaultLibraryCategoryId: (store.settings.defaultLibraryCategoryId ?? null) === cat.id ? null : cat.id })}
|
onkeydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { editingId = null; } }}
|
||||||
title={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "Remove as default folder" : "Set as default folder"}>
|
onblur={commitEdit} use:focusInput />
|
||||||
<Star size={13} weight={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "fill" : "light"} />
|
<button class="s-btn-icon" onclick={commitEdit} title="Save">✓</button>
|
||||||
</button>
|
{:else}
|
||||||
<button class="s-btn-icon"
|
{@const isDefault = (store.settings.defaultLibraryCategoryId ?? null) === cat.id}
|
||||||
onclick={() => toggleHiddenCategory(cat.id)}
|
{@const isHidden = (store.settings.hiddenCategoryIds ?? []).includes(cat.id)}
|
||||||
title={(store.settings.hiddenCategoryIds ?? []).includes(cat.id) ? "Show in Saved tab" : "Hide from Saved tab"}>
|
{@const inUpdate = cat.includeInUpdate !== false}
|
||||||
{#if (store.settings.hiddenCategoryIds ?? []).includes(cat.id)}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
{@const inDl = cat.includeInDownload !== false}
|
||||||
</button>
|
|
||||||
<button
|
<div class="s-folder-identity" draggable="true"
|
||||||
class="s-btn-icon"
|
ondragstart={(e) => onDragStart(e, cat.id)}
|
||||||
class:accent={cat.includeInUpdate !== false}
|
ondragend={onDragEnd}>
|
||||||
onclick={() => toggleCategoryFlag(cat.id, "includeInUpdate")}
|
<span class="s-folder-icon">
|
||||||
title={cat.includeInUpdate !== false ? "Exclude from library updates" : "Include in library updates"}>
|
<FolderSimple size={14} weight="light" />
|
||||||
<ArrowsClockwise size={13} weight={cat.includeInUpdate !== false ? "bold" : "light"} />
|
<DotsSixVertical size={14} weight="bold" />
|
||||||
</button>
|
</span>
|
||||||
<button
|
<span class="s-folder-name" onclick={(e) => { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">
|
||||||
class="s-btn-icon"
|
{cat.name}
|
||||||
class:accent={cat.includeInDownload !== false}
|
</span>
|
||||||
onclick={() => toggleCategoryFlag(cat.id, "includeInDownload")}
|
</div>
|
||||||
title={cat.includeInDownload !== false ? "Exclude from auto-downloads" : "Include in auto-downloads"}>
|
|
||||||
<DownloadSimple size={13} weight={cat.includeInDownload !== false ? "bold" : "light"} />
|
<span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span>
|
||||||
</button>
|
|
||||||
<button class="s-btn-icon" onclick={() => moveCategory(cat.id, -1)} disabled={i === 0} title="Move up">↑</button>
|
<div class="s-folder-actions">
|
||||||
<button class="s-btn-icon" onclick={() => moveCategory(cat.id, 1)} disabled={i === displayCats.length - 1} title="Move down">↓</button>
|
<button class="s-btn-icon" class:active={isDefault}
|
||||||
<button class="s-btn-icon" onclick={() => startEdit(cat.id, cat.name)} title="Rename"><Pencil size={12} weight="light" /></button>
|
onclick={() => updateSettings({ defaultLibraryCategoryId: isDefault ? null : cat.id })}
|
||||||
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete"><Trash size={12} weight="light" /></button>
|
title={isDefault ? "Remove as default folder" : "Set as default folder"}>
|
||||||
{/if}
|
<Star size={13} weight={isDefault ? "fill" : "light"} />
|
||||||
</div>
|
</button>
|
||||||
{/each}
|
|
||||||
|
<button class="s-btn-icon" class:muted={isHidden}
|
||||||
|
onclick={() => toggleHiddenCategory(cat.id)}
|
||||||
|
title={isHidden ? "Show in library" : "Hide from library"}>
|
||||||
|
{#if isHidden}
|
||||||
|
<EyeSlash size={13} weight="light" />
|
||||||
|
{:else}
|
||||||
|
<Eye size={13} weight="light" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="s-btn-icon" class:active={inUpdate} class:inactive={!inUpdate}
|
||||||
|
onclick={() => toggleCategoryFlag(cat.id, "includeInUpdate")}
|
||||||
|
title={inUpdate ? "Included in updates — click to exclude" : "Excluded from updates — click to include"}>
|
||||||
|
{#if inUpdate}
|
||||||
|
<ArrowsClockwise size={13} weight="bold" />
|
||||||
|
{:else}
|
||||||
|
<ArrowsCounterClockwise size={13} weight="light" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="s-btn-icon" class:active={inDl} class:inactive={!inDl}
|
||||||
|
onclick={() => toggleCategoryFlag(cat.id, "includeInDownload")}
|
||||||
|
title={inDl ? "Included in auto-downloads — click to exclude" : "Excluded from auto-downloads — click to include"}>
|
||||||
|
<DownloadSimple size={13} weight={inDl ? "bold" : "light"} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder">
|
||||||
|
<Trash size={12} weight="light" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<style>
|
||||||
|
.s-folder-list {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-list.is-dragging,
|
||||||
|
.s-folder-list.is-dragging * {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-row {
|
||||||
|
transition: opacity 0.15s, background 0.1s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-row.dragging {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-row.drop-above::before,
|
||||||
|
.s-folder-row.drop-below::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-success, #4ade80);
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-identity:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-icon {
|
||||||
|
display: grid;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-icon > :global(*) {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-icon > :global(*:last-child) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-row:hover .s-folder-icon > :global(*:first-child) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-row:hover .s-folder-icon > :global(*:last-child) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-name {
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-name:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-folder-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-btn-icon.active {
|
||||||
|
color: var(--accent, #6c8ef5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-btn-icon.inactive {
|
||||||
|
color: var(--color-error, #f87171);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-btn-icon.inactive:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-btn-icon.muted {
|
||||||
|
color: var(--text-faint);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user