From 13f2a483ca955b738a734e91076ab27cbb13ae4c Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Sun, 31 May 2026 00:30:36 -0500 Subject: [PATCH] Chore: Port over Extensions & Search --- package.json | 1 + pnpm-lock.yaml | 3 + .../components/browse/GenreDrillPage.svelte | 254 ++++++++ src/lib/components/browse/KeywordTab.svelte | 322 ++++++++++ src/lib/components/browse/Search.svelte | 292 +++++++++ src/lib/components/browse/SourceTab.svelte | 376 +++++++++++ src/lib/components/browse/TagTab.svelte | 444 +++++++++++++ src/lib/components/browse/lib/searchFilter.ts | 133 ++++ src/lib/components/chrome/Sidebar.svelte | 36 +- src/lib/components/chrome/Toaster.svelte | 44 +- .../components/downloads/DownloadItem.svelte | 132 ++++ .../components/downloads/DownloadQueue.svelte | 94 +++ src/lib/components/downloads/Downloads.svelte | 228 +++++++ .../downloads/StorageWarningDialog.svelte | 105 ++++ src/lib/components/downloads/lib/autoRetry.ts | 32 + .../components/downloads/lib/downloadQueue.ts | 55 ++ .../extensions/ExtensionCard.svelte | 134 ++++ .../extensions/ExtensionFilters.svelte | 115 ++++ .../extensions/ExtensionLibrary.svelte | 336 ++++++++++ .../components/extensions/Extensions.svelte | 415 ++++++++++++ .../extensions/lib/extensionHelpers.ts | 55 ++ .../extensions/lib/extensionLibrary.ts | 56 ++ .../panels/ExtensionSettingsPanel.svelte | 588 ++++++++++++++++++ .../panels/SourceMigrateModal.svelte | 445 +++++++++++++ src/lib/components/library/Library.svelte | 475 ++++++++++++++ src/lib/components/library/LibraryGrid.svelte | 6 +- .../components/library/LibraryToolbar.svelte | 498 ++++++--------- src/lib/components/series/ChapterList.svelte | 18 +- .../components/series/SeriesActions.svelte | 22 +- src/lib/components/series/SeriesDetail.svelte | 208 +++---- src/lib/components/series/SeriesHeader.svelte | 35 +- .../shared/manga/MangaPreview.svelte | 11 +- src/lib/core/filesystem.ts | 48 ++ src/lib/request-manager/downloads.ts | 58 +- src/lib/server-adapters/suwayomi/index.ts | 122 +++- src/lib/server-adapters/suwayomi/types.ts | 3 +- src/lib/server-adapters/types.ts | 7 +- src/lib/state/app.svelte.ts | 28 +- src/lib/state/downloads.svelte.ts | 352 ++++++++++- src/lib/state/library.svelte.ts | 7 + src/routes/+layout.svelte | 23 + src/routes/browse/+page.svelte | 18 +- src/routes/browse/[sourceId]/+page.svelte | 9 +- src/routes/downloads/+page.svelte | 6 +- src/routes/extensions/+page.svelte | 6 +- src/routes/library/+page.svelte | 435 +------------ src/routes/series/[mangaid]/+page.svelte | 12 +- 47 files changed, 6086 insertions(+), 1016 deletions(-) create mode 100644 src/lib/components/browse/GenreDrillPage.svelte create mode 100644 src/lib/components/browse/KeywordTab.svelte create mode 100644 src/lib/components/browse/Search.svelte create mode 100644 src/lib/components/browse/SourceTab.svelte create mode 100644 src/lib/components/browse/TagTab.svelte create mode 100644 src/lib/components/browse/lib/searchFilter.ts create mode 100644 src/lib/components/downloads/DownloadItem.svelte create mode 100644 src/lib/components/downloads/DownloadQueue.svelte create mode 100644 src/lib/components/downloads/Downloads.svelte create mode 100644 src/lib/components/downloads/StorageWarningDialog.svelte create mode 100644 src/lib/components/downloads/lib/autoRetry.ts create mode 100644 src/lib/components/downloads/lib/downloadQueue.ts create mode 100644 src/lib/components/extensions/ExtensionCard.svelte create mode 100644 src/lib/components/extensions/ExtensionFilters.svelte create mode 100644 src/lib/components/extensions/ExtensionLibrary.svelte create mode 100644 src/lib/components/extensions/Extensions.svelte create mode 100644 src/lib/components/extensions/lib/extensionHelpers.ts create mode 100644 src/lib/components/extensions/lib/extensionLibrary.ts create mode 100644 src/lib/components/extensions/panels/ExtensionSettingsPanel.svelte create mode 100644 src/lib/components/extensions/panels/SourceMigrateModal.svelte create mode 100644 src/lib/components/library/Library.svelte create mode 100644 src/lib/core/filesystem.ts diff --git a/package.json b/package.json index 452da82..c384836 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@tauri-apps/plugin-store": "^2.4.3", "capacitor-native-biometric": "^4.2.2", + "clsx": "^2.1.1", "phosphor-svelte": "^3.1.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52b1b83..17d4539 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: capacitor-native-biometric: specifier: ^4.2.2 version: 4.2.2 + clsx: + specifier: ^2.1.1 + version: 2.1.1 phosphor-svelte: specifier: ^3.1.0 version: 3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) diff --git a/src/lib/components/browse/GenreDrillPage.svelte b/src/lib/components/browse/GenreDrillPage.svelte new file mode 100644 index 0000000..d81870e --- /dev/null +++ b/src/lib/components/browse/GenreDrillPage.svelte @@ -0,0 +1,254 @@ + + +
+
+ + {label} + {#if !loadingInitial || filtered.length > 0} + {visibleItems.length}{filtered.length > visibleCount ? "+" : ""} of {filtered.length} + {/if} + {#if !loadingInitial && hasMoreNetwork} + More loading… + {/if} +
+ + {#if loadingInitial && filtered.length === 0} +
+ {#each Array(50) as _} +
+
+
+
+ {/each} +
+ {:else if filtered.length === 0} +
No manga found for "{label}".
+ {:else} +
+ {#each visibleItems as m, i (m.id)} + + {/each} + {#if hasMore} +
+ +
+ {/if} +
+ {/if} +
+ +{#if ctx} + ctx = null} /> +{/if} + + \ No newline at end of file diff --git a/src/lib/components/browse/KeywordTab.svelte b/src/lib/components/browse/KeywordTab.svelte new file mode 100644 index 0000000..c85aeee --- /dev/null +++ b/src/lib/components/browse/KeywordTab.svelte @@ -0,0 +1,322 @@ + + +
+ + + {#if kw_showAdvanced && hasMultipleLangs} +
+
+ LANGUAGES +
+ + +
+
+
+ {#each availableLangs as lang (lang)} + + {/each} +
+
+
+ Searching {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""} +
+
+ {/if} +
+ +{#if !query.trim()} + {#if popularLoading && popularResults.length === 0} +
+ {#each Array(24) as _, i (i)}
{/each} +
+ {:else if popularResults.length > 0} +
+ Popular right now +
+
+ {#each popularResults as m (m.id)} + + {/each} + {#if popularLoading} + {#each Array(12) as _, i (i)}
{/each} + {/if} +
+ {:else} +
+ +

Search across sources

+

+ {#if hasMultipleLangs} + {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""} · {kw_selectedLangs.size} language{kw_selectedLangs.size !== 1 ? "s" : ""} + {:else} + {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""} + {/if} +

+
+ {/if} +{:else} + {#if kw_flatResults.length > 0} +
+ {kw_flatResults.length} result{kw_flatResults.length !== 1 ? "s" : ""} +
+
+ {#each kw_flatResults as m (m.id)} + + {/each} + {#if kw_anyLoading} + {#each Array(6) as _, i (i)}
{/each} + {/if} +
+ {:else if kw_anyLoading} +
+ {#each Array(12) as _, i (i)}
{/each} +
+ {:else if kw_allDone && !kw_hasResults} +
+

No results for "{query.trim()}"

+

Try a different spelling or fewer words

+
+ {/if} +{/if} + + + + \ No newline at end of file diff --git a/src/lib/components/browse/Search.svelte b/src/lib/components/browse/Search.svelte new file mode 100644 index 0000000..370bb62 --- /dev/null +++ b/src/lib/components/browse/Search.svelte @@ -0,0 +1,292 @@ + + +
+
+ Search + +
+ {#if anims && tabIndicator.width > 0} + + {/if} + + + +
+
+ + {#if urlTab === "keyword"} + setPreviewManga(m)} + /> + {:else if urlTab === "tag"} + setPreviewManga(m)} + onGenreDrill={(genre) => goto(`/browse?genre=${encodeURIComponent(genre)}`)} + /> + {:else} + setPreviewManga(m)} + /> + {/if} +
+ + \ No newline at end of file diff --git a/src/lib/components/browse/SourceTab.svelte b/src/lib/components/browse/SourceTab.svelte new file mode 100644 index 0000000..eb74c9a --- /dev/null +++ b/src/lib/components/browse/SourceTab.svelte @@ -0,0 +1,376 @@ + + +
+
+
+ Language + +
+ + {#if loadingSources} +
+ +
+ {:else} +
+ {#if localSource} + +
+ {/if} + + {#if pinnedSources.length > 0} + + {#each pinnedSources as src (src.id)} + + {/each} +
+ + {/if} + + {#each src_visibleSources as src (src.id)} + + {/each} + {#if src_visibleSources.length === 0} +

No sources for this language

+ {/if} +
+ {/if} +
+ +
+ {#if !src_activeSource} +
+ +

Browse a source

+

Select a source to see its popular titles, or search within it.

+
+ {:else} +
+
+ { (e.target as HTMLImageElement).style.display = "none"; }} /> + {src_activeSource.displayName} + {#if src_loadingBrowse} + + {:else if src_browseResults.length > 0} + {src_browseResults.length} results + {/if} +
+
+ +
+ + +
+ + {#if src_loadingBrowse && src_browseResults.length === 0} +
+ {#each Array(18) as _, i (i)} +
+ {/each} +
+ {:else if src_browseResults.length > 0} +
+ {#each src_browseResults as m, i (m.id)} + + {/each} + {#if src_hasNextPage} +
+ +
+ {/if} +
+ {:else if !src_loadingBrowse} +
+

No results

+

Try a different search term.

+
+ {/if} + {/if} +
+
+ +{#if ctx_source} + {@const isPinned = pinnedIds.includes(ctx_source.id)} + { togglePinnedSource(ctx_source!.id); }, + }, + { separator: true }, + { + label: "Browse source", + icon: ArrowRight, + onClick: () => { srcSelectSource(ctx_source!); }, + }, + ]} + /> +{/if} + + \ No newline at end of file diff --git a/src/lib/components/browse/TagTab.svelte b/src/lib/components/browse/TagTab.svelte new file mode 100644 index 0000000..05427ac --- /dev/null +++ b/src/lib/components/browse/TagTab.svelte @@ -0,0 +1,444 @@ + + +
+ +
+
+ + + {#if tag_tagFilter} + + {/if} +
+
+
Status
+ {#each MANGA_STATUSES as { value, label } (value)} + + {/each} +
Genre
+ {#each tag_filteredGenres as tag (tag)} + + {/each} + {#if tag_filteredGenres.length === 0} +

No matching genres

+ {/if} +
+
+ +
+ {#if !tag_hasActiveFilters} +
+ +

Browse by tag

+

Select a status or genre to find matching manga.

+
+ {:else} + +
+
+ {#each tag_activeStatuses as status (status)} + + {MANGA_STATUSES.find((s) => s.value === status)?.label ?? status} + + + {/each} + {#each tag_activeTags as tag (tag)} + + {tag} + + + {/each} +
+
+ {#if tag_activeTags.length > 1} +
+ + +
+ {/if} + + +
+
+ +
+ + {#if tag_activeStatuses.length > 0 && tag_activeTags.length === 0} + {tag_activeStatuses.map((s) => MANGA_STATUSES.find((x) => x.value === s)?.label ?? s).join(" · ")} + {:else if tag_activeTags.length === 1 && tag_activeStatuses.length === 0} + {tag_activeTags[0]} + {:else} + {[...tag_activeStatuses.map((s) => MANGA_STATUSES.find((x) => x.value === s)?.label ?? s), ...tag_activeTags].join(` ${tag_tagMode} `)} + {/if} + {#if tag_searchSources} + + sources + {/if} + + {#if tag_loadingLocal} + + {:else} + + {tag_totalVisible}{tag_localHasNext ? "+" : ""} results + {#if tag_searchSources && sourceCacheReady} + · {sourceCache.size} cached + {/if} + + {/if} +
+ + {#if tag_loadingLocal} +
+ {#each Array(48) as _, i (i)} +
+ {/each} +
+ {:else if tag_mergedResults.length > 0} +
+ {#each tag_mergedResults as m, i (m.id)} + + {/each} + {#if tag_loadingMoreLocal} + {#each Array(12) as _, i (i)} +
+ {/each} + {/if} +
+ {:else} +
+

No results

+

+ {#if tag_searchSources}Try OR mode or broader tags. + {:else}Try OR mode, enable Sources, or check your library. + {/if} +

+
+ {/if} + {/if} +
+
+ + \ No newline at end of file diff --git a/src/lib/components/browse/lib/searchFilter.ts b/src/lib/components/browse/lib/searchFilter.ts new file mode 100644 index 0000000..28fbecd --- /dev/null +++ b/src/lib/components/browse/lib/searchFilter.ts @@ -0,0 +1,133 @@ +import type { Settings } from "$lib/types/settings"; +import { shouldHideNsfw } from "$lib/core/util"; + +export { shouldHideNsfw }; + +export const PAGE_SIZE = 50; +export const INITIAL_PAGES = 3; +export const MAX_SOURCES = 12; +export const CONCURRENCY = 4; + +export function parseTags(f: string): string[] { + return f.split("+").map((t) => t.trim()).filter(Boolean); +} + +export function tagsLabel(tags: string[]): string { + if (tags.length === 1) return tags[0]; + return tags.slice(0, -1).join(", ") + " & " + tags[tags.length - 1]; +} + +export function matchesAllTags(m: { genre?: string[] }, tags: string[]): boolean { + const g = (m.genre ?? []).map((x) => x.toLowerCase()); + return tags.every((t) => g.includes(t.toLowerCase())); +} + +export async function runConcurrent( + items: T[], + fn: (item: T) => Promise, + signal: AbortSignal, +): Promise { + let i = 0; + async function worker() { + while (i < items.length) { + if (signal.aborted) return; + await fn(items[i++]).catch(() => {}); + } + } + await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker)); +} + +export type TagMode = "AND" | "OR"; + +export interface CachedManga { + id: number; + title: string; + thumbnailUrl: string; + inLibrary: boolean; + status: string; + genre: string[]; + lowerGenres: string[]; + sourceId: string; + genreEnriched: boolean; +} + +export const COMMON_GENRES = [ + "Action", "Adventure", "Comedy", "Drama", "Fantasy", "Romance", + "Sci-Fi", "Slice of Life", "Horror", "Mystery", "Thriller", "Sports", + "Supernatural", "Mecha", "Historical", "Psychological", "School Life", + "Shounen", "Seinen", "Josei", "Shoujo", "Isekai", "Martial Arts", + "Magic", "Music", "Cooking", "Medical", "Military", "Harem", "Ecchi", +] as const; + +export const MANGA_STATUSES: { value: string; label: string }[] = [ + { value: "ONGOING", label: "Ongoing" }, + { value: "COMPLETED", label: "Completed" }, + { value: "HIATUS", label: "Hiatus" }, + { value: "ABANDONED", label: "Abandoned" }, + { value: "UNKNOWN", label: "Unknown" }, +]; + +export function buildTagFilter( + tags: string[], + mode: TagMode, + statuses: string[], +): Record { + const genrePart: Record | null = + tags.length === 0 ? null : + mode === "AND" + ? { and: tags.map((t) => ({ genre: { includesInsensitive: t } })) } + : { or: tags.map((t) => ({ genre: { includesInsensitive: t } })) }; + + const statusPart: Record | null = + statuses.length === 0 ? null : + statuses.length === 1 + ? { status: { equalTo: statuses[0] } } + : { or: statuses.map((s) => ({ status: { equalTo: s } })) }; + + if (!genrePart && !statusPart) return {}; + if (genrePart && !statusPart) return genrePart; + if (!genrePart && statusPart) return statusPart; + return { and: [genrePart, statusPart] }; +} + +export function filterSourceCache( + sourceCache: Map, + tags: string[], + mode: TagMode, + statuses: string[], + settings: Pick, +): CachedManga[] { + return [...sourceCache.values()].filter((m) => { + if (shouldHideNsfw(m as any, settings)) return false; + + const statusMatch = statuses.length === 0 || statuses.includes(m.status); + + let genreMatch = true; + if (tags.length > 0) { + const lower = m.lowerGenres; + genreMatch = mode === "AND" + ? tags.every((t) => lower.some((g) => g.includes(t.toLowerCase()))) + : tags.some((t) => lower.some((g) => g.includes(t.toLowerCase()))); + } + + return statusMatch && genreMatch; + }); +} + +export function toCachedManga( + m: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean; genre?: string[]; status?: string }, + srcId: string, +): CachedManga { + const genre = m.genre ?? []; + return { + id: m.id, + title: m.title, + thumbnailUrl: m.thumbnailUrl, + inLibrary: m.inLibrary, + status: m.status ?? "UNKNOWN", + genre, + lowerGenres: genre.map((g) => g.toLowerCase()), + sourceId: srcId, + genreEnriched: genre.length > 0, + }; +} \ No newline at end of file diff --git a/src/lib/components/chrome/Sidebar.svelte b/src/lib/components/chrome/Sidebar.svelte index 13fbebe..90216a4 100644 --- a/src/lib/components/chrome/Sidebar.svelte +++ b/src/lib/components/chrome/Sidebar.svelte @@ -1,33 +1,33 @@