From dd0cf9372d94b27c8fab8d6257985609f832b2f1 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Wed, 29 Apr 2026 18:18:21 -0500 Subject: [PATCH] Feat: Implement CSS for Chrome & Link to Context-Menu --- Todo | 20 ++- src/design/tokens/spacing.css | 5 +- .../library/components/Library.svelte | 76 ++++++--- src/shared/chrome/TitleBar.svelte | 4 +- src/shared/ui/ContextMenu.svelte | 156 ++++++++++++++---- src/store/state.svelte.ts | 7 + 6 files changed, 206 insertions(+), 62 deletions(-) diff --git a/Todo b/Todo index f552946..ad4f87f 100644 --- a/Todo +++ b/Todo @@ -7,10 +7,7 @@ Major Revisions: Minor Revisions: - Investigate feasibility of Multi-Page Screenshot (Reader) - - Add Hover Info on Library (Make sure doesn't conflict with additional clicks) - Revise Migration (https://github.com/Suwayomi/Suwayomi-WebUI/pull/1073) - - Look at how Manga are Organized in WebUI and Implement into Series-Detail (Chapter Display is Off) - Priority Bugs: - Fix Library-Refresh System (TESTING) @@ -24,6 +21,10 @@ Pending/On-Hold: - Working on 3D Display Cards - Add Flathub Support (Pending Video) + - Change Auto-Link Threshold + - Fix Auto-Link De-dupe for Images + - Optimize Auto-Link Latency (IP) + In-Progress: - Fix Tracking Login - Pasting OAuth URL is not User-Friendly, Look for Alternatives @@ -31,14 +32,19 @@ In-Progress: - Tracking - Fix SeriesDetail Tracking Window (Maybe Link to TrackingPanel) - - Refactor Content-Filters, Change Source-Enabling (More Intuitive) - - Integrate Tauri JSON for Settings - Create Migration Logic for Local Storage - Integrate Tauri SQLITE for Caching - Document Caching (What is being Loaded) - Create Proper Cache-Clearing & Tie into Suwayomi Methods + - Fix Tap-Hold to Context-Menu Instead of Select + - Cap Context-Menu Folders (Sub-In Move-Custom for Additionals) + + - Add Multi-Select for SeriesDetail GridView + Notes from last time: - - Fixed Search Issue - - Create Z-Index for Library Statistics + - Storage has been configured, now just need protocols + - Export/Import + - Migration + - Data-Clean \ No newline at end of file diff --git a/src/design/tokens/spacing.css b/src/design/tokens/spacing.css index 83ce6dd..4f8acc8 100644 --- a/src/design/tokens/spacing.css +++ b/src/design/tokens/spacing.css @@ -8,5 +8,6 @@ --sp-8: 32px; --sp-10: 40px; - --sidebar-width: 52px; -} + --sidebar-width: 52px; + --titlebar-height: 36px; +} \ No newline at end of file diff --git a/src/features/library/components/Library.svelte b/src/features/library/components/Library.svelte index a76c35a..46014da 100644 --- a/src/features/library/components/Library.svelte +++ b/src/features/library/components/Library.svelte @@ -19,7 +19,7 @@ } from "../store/libraryState.svelte"; import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter, LibraryUpdateEntry } from "@store/state.svelte"; import type { Manga, Category, Chapter } from "@types"; - import { checkAndMarkCompleted as storeCheckAndMarkCompleted } from "@store/state.svelte"; + import { checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings } from "@store/state.svelte"; import LibraryToolbar from "./LibraryToolbar.svelte"; import LibraryGrid from "./LibraryGrid.svelte"; @@ -32,6 +32,7 @@ const CARD_MIN_W = 130; const CARD_GAP = 16; const COMPLETED_NAME = "Completed"; + const CTX_FOLDER_CAP = 4; const paginator = createPaginator(store.settings.renderLimit ?? 48); @@ -68,11 +69,10 @@ let dragTabId: number | null = $state(null); let dragOverTabId: number | null = $state(null); - const DT_TAB = "application/x-moku-tab"; const anims = $derived(store.settings.qolAnimations ?? true); - const tab = $derived(store.libraryFilter); + const tab = $derived(store.libraryFilter); const tabSortMode = $derived(store.settings.libraryTabSort[tab]?.mode ?? "az" as LibrarySortMode); const tabSortDir = $derived(store.settings.libraryTabSort[tab]?.dir ?? "asc" as LibrarySortDir); const tabStatus = $derived(store.settings.libraryTabStatus[tab] ?? "ALL" as LibraryStatusFilter); @@ -198,14 +198,33 @@ function loadMore() { renderVisible = paginator.nextVisible(renderVisible); } let longPressTimer: ReturnType | null = null; + let longPressFired = false; + let emptyLongPressTimer: ReturnType | null = null; + + function onRootPointerDown(e: PointerEvent) { + if (e.button !== 0) return; + if ((e.target as HTMLElement).closest("button, .card")) return; + emptyLongPressTimer = setTimeout(() => { + emptyLongPressTimer = null; + emptyCtx = { x: e.clientX - SIDEBAR_W, y: e.clientY - TITLEBAR_H }; + }, 500); + } + function onRootPointerUp() { if (emptyLongPressTimer) { clearTimeout(emptyLongPressTimer); emptyLongPressTimer = null; } } + function onRootPointerLeave() { if (emptyLongPressTimer) { clearTimeout(emptyLongPressTimer); emptyLongPressTimer = null; } } function onCardPointerDown(e: PointerEvent, m: Manga) { if (e.button !== 0) return; - longPressTimer = setTimeout(() => { longPressTimer = null; enterSelectMode(m.id); }, 500); + longPressFired = false; + longPressTimer = setTimeout(() => { + longPressTimer = null; + longPressFired = true; + ctx = { x: e.clientX - SIDEBAR_W, y: e.clientY - TITLEBAR_H, manga: m }; + }, 500); } function onCardPointerUp() { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } } function onCardPointerLeave() { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } } function onCardClick(e: MouseEvent, m: Manga) { + if (longPressFired) { longPressFired = false; return; } if (selectMode) { toggleSelect(m.id); return; } if (e.metaKey || e.ctrlKey || e.shiftKey) { e.preventDefault(); enterSelectMode(m.id); return; } store.activeManga = m; @@ -271,6 +290,11 @@ } catch (e) { console.error(e); } } + function bumpCategoryFrecency(catId: number) { + const prev = (store.settings as any).categoryFrecency ?? {}; + updateSettings({ categoryFrecency: { ...prev, [catId]: (prev[catId] ?? 0) + 1 } } as any); + } + async function toggleMangaCategory(manga: Manga, cat: Category) { const inCat = (categoryMangaMap.get(cat.id) ?? []).some(m => m.id === manga.id); setCategories(store.categories.map(c => { @@ -278,6 +302,7 @@ const nodes = inCat ? c.mangas.nodes.filter(m => m.id !== manga.id) : [...c.mangas.nodes, manga]; return { ...c, mangas: { nodes } }; })); + if (!inCat) bumpCategoryFrecency(cat.id); try { await gql(UPDATE_MANGA_CATEGORIES, { mangaId: manga.id, addTo: inCat ? [] : [cat.id], removeFrom: inCat ? [cat.id] : [] }); if (!inCat && !manga.inLibrary) { @@ -296,6 +321,7 @@ const res = await gql<{ createCategory: { category: Category } }>(CREATE_CATEGORY, { name: name.trim() }); const cat = res.createCategory.category; await gql(UPDATE_MANGA_CATEGORIES, { mangaId: manga.id, addTo: [cat.id], removeFrom: [] }); + bumpCategoryFrecency(cat.id); if (!manga.inLibrary) { await gql(UPDATE_MANGA, { id: manga.id, inLibrary: true }); allManga = allManga.map(m => m.id === manga.id ? { ...m, inLibrary: true } : m); @@ -342,24 +368,37 @@ catch (e: any) { addToast({ kind: "error", title: "Could not open folder", body: e?.toString?.() ?? path }); } } + const SIDEBAR_W = 52; + const TITLEBAR_H = 36; + function openCtx(e: MouseEvent, m: Manga) { if (selectMode) { toggleSelect(m.id); return; } e.preventDefault(); - ctx = { x: e.clientX, y: e.clientY, manga: m }; + ctx = { x: e.clientX - SIDEBAR_W, y: e.clientY - TITLEBAR_H, manga: m }; } function buildCtxItems(m: Manga): MenuEntry[] { - const catEntries: MenuEntry[] = visibleCategories.map(cat => { + const frecency: Record = (store.settings as any).categoryFrecency ?? {}; + const sorted = [...visibleCategories].sort((a, b) => (frecency[b.id] ?? 0) - (frecency[a.id] ?? 0)); + const pinned = sorted.slice(0, CTX_FOLDER_CAP); + const overflow = sorted.slice(CTX_FOLDER_CAP); + + const makeCatEntry = (cat: Category): MenuEntry => { const inCat = (categoryMangaMap.get(cat.id) ?? []).some(x => x.id === m.id); - return { label: inCat ? `Remove from ${cat.name}` : `Add to ${cat.name}`, icon: Folder, onClick: () => toggleMangaCategory(m, cat) }; - }); + return { label: inCat ? `Remove from ${cat.name}` : cat.name, icon: Folder, onClick: () => toggleMangaCategory(m, cat) }; + }; + + const pinnedEntries = pinned.map(makeCatEntry); + const overflowChildren: MenuEntry[] = overflow.map(makeCatEntry); + return [ { label: m.inLibrary ? "Remove from library" : "Add to library", icon: Books, onClick: () => m.inLibrary ? removeFromLibrary(m) : gql(UPDATE_MANGA, { id: m.id, inLibrary: true }).then(() => { allManga = allManga.map(x => x.id === m.id ? { ...x, inLibrary: true } : x); cache.clear(CACHE_KEYS.LIBRARY); }).catch(console.error) }, { label: "Open in file manager", icon: ArrowSquareOut, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => openMangaFolder(m) }, { label: "Delete all downloads", icon: Trash, danger: true, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => deleteAllDownloads(m) }, { separator: true }, - { label: "Select this manga", icon: CheckSquare, onClick: () => enterSelectMode(m.id) }, - ...(catEntries.length ? [{ separator: true } as MenuEntry, ...catEntries] : []), + { label: "Select", icon: CheckSquare, onClick: () => enterSelectMode(m.id) }, + ...(pinnedEntries.length ? [{ separator: true } as MenuEntry, ...pinnedEntries] : []), + ...(overflowChildren.length ? [{ label: `More folders (${overflowChildren.length})`, icon: FolderSimple, onClick: () => {}, children: overflowChildren } as MenuEntry] : []), { separator: true }, { label: "New folder", icon: FolderSimplePlus, onClick: () => createAndAssign(m) }, ]; @@ -393,12 +432,9 @@ refreshProgress = { finished: 0, total: 0 }; cancelUpdate = startLibraryUpdate({ - onProgress(p) { - refreshProgress = p; - }, + onProgress(p) { refreshProgress = p; }, async onDone({ entries, totalUpdated, newChapters }) { - refreshing = false; - cancelUpdate = null; + refreshing = false; cancelUpdate = null; setLibraryUpdates(entries); cache.clearGroup(CACHE_GROUPS.LIBRARY); await loadData(); @@ -407,10 +443,7 @@ refreshDoneTimer = setTimeout(() => { refreshDone = false; }, 2500); showToast(newChapters, totalUpdated); }, - onError() { - refreshing = false; - cancelUpdate = null; - }, + onError() { refreshing = false; cancelUpdate = null; }, }); } @@ -493,8 +526,11 @@ oncontextmenu={(e) => { if ((e.target as HTMLElement).closest("button")) return; e.preventDefault(); - emptyCtx = { x: e.clientX, y: e.clientY }; + emptyCtx = { x: e.clientX - SIDEBAR_W, y: e.clientY - TITLEBAR_H }; }} + onpointerdown={onRootPointerDown} + onpointerup={onRootPointerUp} + onpointerleave={onRootPointerLeave} > {#if store.settings.libraryBranches ?? true} @@ -117,12 +195,27 @@ box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.35), 0 16px 40px rgba(0,0,0,0.25); animation: scaleIn 0.1s ease both; transform-origin: top left; } + .item-wrap { position: relative; } + .submenu { + position: absolute; + left: 100%; + top: 0; + z-index: 201; + animation: scaleIn 0.08s ease both; + transform-origin: top left; + } + .submenu.sub-flip { + left: auto; + right: 100%; + transform-origin: top right; + } .item { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 5px var(--sp-2); border-radius: var(--radius-md); font-size: var(--text-sm); color: var(--text-secondary); text-align: left; cursor: pointer; background: none; border: none; outline: none; transition: background var(--t-fast), color var(--t-fast); + position: relative; } .item:hover:not(.disabled), .item.focused:not(.disabled) { background: var(--bg-overlay); color: var(--text-primary); } .item.danger { color: var(--color-error); } @@ -131,6 +224,7 @@ .icon { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex-shrink: 0; color: var(--text-faint); border-radius: var(--radius-sm); } .icon-danger { color: var(--color-error); opacity: 0.7; } .label { flex: 1; line-height: 1.3; } + .sub-arrow { font-size: 14px; color: var(--text-faint); line-height: 1; margin-left: auto; padding-left: var(--sp-1); } .sep { height: 1px; background: var(--border-dim); margin: 3px var(--sp-1); } @keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } } diff --git a/src/store/state.svelte.ts b/src/store/state.svelte.ts index 18d4d1b..1bbc583 100644 --- a/src/store/state.svelte.ts +++ b/src/store/state.svelte.ts @@ -48,6 +48,7 @@ function mergeSettings(saved: any): Settings { pinnedSourceIds: saved?.settings?.pinnedSourceIds ?? [], readerPresets: saved?.settings?.readerPresets ?? [], mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {}, + categoryFrecency: saved?.settings?.categoryFrecency ?? {}, }; } @@ -317,6 +318,11 @@ class Store { this.settings = { ...this.settings, mangaReaderSettings: next }; } + bumpCategoryFrecency(catId: number) { + const prev = this.settings.categoryFrecency ?? {}; + this.settings = { ...this.settings, categoryFrecency: { ...prev, [catId]: (prev[catId] ?? 0) + 1 } }; + } + setCategories(cats: Category[]) { this.categories = cats; } setActiveManga(next: Manga | null) { this.activeManga = next; } setPreviewManga(next: Manga | null) { this.previewManga = next; } @@ -355,6 +361,7 @@ export function updateReaderPreset(id: string, patch: Partial) { store.updateSettings(patch); } export function resetKeybinds() { store.resetKeybinds(); } export function clearSearchCache() { store.clearSearchCache(); }