From f49f7e7ac15434bd39b66ea4cf63d672044207c5 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Thu, 2 Apr 2026 00:56:27 -0500 Subject: [PATCH] Feat: Automation Panel (WIP) & SeriesDetail Additions --- Todo | 3 + src/components/pages/Search.svelte | 133 ++++- src/components/pages/SeriesDetail.svelte | 554 ++++++++++--------- src/components/pages/Tracking.svelte | 103 ++-- src/components/shared/AutomationPanel.svelte | 321 +++++++++++ src/components/shared/MangaPreview.svelte | 27 +- src/components/shared/TrackingPanel.svelte | 118 ++-- src/store/state.svelte.ts | 286 ++++------ 8 files changed, 962 insertions(+), 583 deletions(-) create mode 100644 src/components/shared/AutomationPanel.svelte diff --git a/Todo b/Todo index 879df8e..d02d9c2 100644 --- a/Todo +++ b/Todo @@ -16,6 +16,7 @@ Minor Revisions: Priority Bugs: - Cache ALL Cover Pictures & Details for Manga in Library + - Fix Library Build not Updating General/Misc Bugs: @@ -29,6 +30,8 @@ General/Misc Bugs: In-Progress: - Enable Cloudflare Bypass (Suwayomi Config) (Requires Patching) + - Patch Migrate Modal to Fill Language Options, not Limit to 7-9. + diff --git a/src/components/pages/Search.svelte b/src/components/pages/Search.svelte index d72cb85..b7c084f 100644 --- a/src/components/pages/Search.svelte +++ b/src/components/pages/Search.svelte @@ -27,6 +27,14 @@ "Magic","Music","Cooking","Medical","Military","Harem","Ecchi", ]; + 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" }, + ]; + async function runConcurrent(items: T[], fn: (item: T) => Promise, signal: AbortSignal): Promise { let i = 0; async function worker() { @@ -44,10 +52,27 @@ return tags.every((t) => genres.includes(t.toLowerCase())); } - function buildGenreFilter(tags: string[], mode: TagMode): Record { - if (tags.length === 0) return {}; - if (mode === "AND") return { and: tags.map((t) => ({ genre: { includesInsensitive: t } })) }; - return { or: tags.map((t) => ({ genre: { includesInsensitive: t } })) }; + 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] }; } const MANGAS_BY_GENRE = ` @@ -168,6 +193,7 @@ const kw_allDone = $derived(kw_results.length > 0 && kw_results.every((r) => !r.loading)); let tag_activeTags: string[] = $state([]); + let tag_activeStatuses: string[] = $state([]); let tag_tagMode: TagMode = $state("AND"); let tag_tagFilter = $state(""); @@ -191,7 +217,7 @@ return q ? COMMON_GENRES.filter((g) => g.toLowerCase().includes(q)) : COMMON_GENRES; }); - const tag_hasActiveTags = $derived(tag_activeTags.length > 0); + const tag_hasActiveTags = $derived(tag_activeTags.length > 0 || tag_activeStatuses.length > 0); const tag_localIds = $derived(new Set(tag_localResults.map((m) => m.id))); const tag_mergedResults = $derived(dedupeMangaByTitle(dedupeMangaById( tag_searchSources @@ -202,9 +228,10 @@ const tag_sourceHasMore = $derived(tag_searchSources && [...tag_srcNextPage.values()].some((p) => p > 0)); $effect(() => { - const _activeTags = tag_activeTags; - const _tagMode = tag_tagMode; - untrack(() => tagFetchLocal(_activeTags, _tagMode)); + const _activeTags = tag_activeTags; + const _tagMode = tag_tagMode; + const _activeStatuses = tag_activeStatuses; + untrack(() => tagFetchLocal(_activeTags, _tagMode, _activeStatuses)); }); let tag_autoSearchFired = $state(false); @@ -223,8 +250,8 @@ } }); - async function tagFetchLocal(activeTags: string[], tagMode: TagMode) { - if (activeTags.length === 0) { + async function tagFetchLocal(activeTags: string[], tagMode: TagMode, activeStatuses: string[]) { + if (activeTags.length === 0 && activeStatuses.length === 0) { tag_localResults = []; tag_totalCount = 0; tag_localHasNext = false; tag_localOffset = 0; return; } @@ -235,7 +262,7 @@ tag_loadingLocal = true; gql<{ mangas: { nodes: Manga[]; pageInfo: { hasNextPage: boolean }; totalCount: number } }>( MANGAS_BY_GENRE, - { filter: buildGenreFilter(activeTags, tagMode), first: (store.settings.renderLimit ?? 48), offset: 0 }, + { filter: buildTagFilter(activeTags, tagMode, activeStatuses), first: (store.settings.renderLimit ?? 48), offset: 0 }, ctrl.signal, ).then((d) => { if (ctrl.signal.aborted) return; @@ -279,7 +306,8 @@ const matching = (activeTags.length > 1 ? result.mangas.filter((m) => matchesAllTags(m, activeTags)) : result.mangas - ).filter((m) => !shouldHideNsfw(m, store.settings)); + ).filter((m) => !shouldHideNsfw(m, store.settings)) + .filter((m) => tag_activeStatuses.length === 0 || tag_activeStatuses.includes(m.status ?? "UNKNOWN")); if (matching.length > 0) { tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks); tag_loadingSourceSearch = false; @@ -298,11 +326,12 @@ try { const d = await gql<{ mangas: { nodes: Manga[]; pageInfo: { hasNextPage: boolean } } }>( MANGAS_BY_GENRE, - { filter: buildGenreFilter(tag_activeTags, tag_tagMode), first: (store.settings.renderLimit ?? 48), offset: tag_localOffset }, + { filter: buildTagFilter(tag_activeTags, tag_tagMode, tag_activeStatuses), first: (store.settings.renderLimit ?? 48), offset: tag_localOffset }, ctrl.signal, ); if (ctrl.signal.aborted) return; const nsfwFilter = (m: Manga) => !shouldHideNsfw(m, store.settings); + tag_localResults = [...tag_localResults, ...d.mangas.nodes.filter(nsfwFilter)]; tag_localHasNext = d.mangas.pageInfo.hasNextPage; tag_localOffset += (store.settings.renderLimit ?? 48); } catch (e: any) { @@ -341,7 +370,8 @@ const matching = (tag_activeTags.length > 1 ? result.mangas.filter((m) => matchesAllTags(m, tag_activeTags)) : result.mangas - ).filter((m) => !shouldHideNsfw(m, store.settings)); + ).filter((m) => !shouldHideNsfw(m, store.settings)) + .filter((m) => tag_activeStatuses.length === 0 || tag_activeStatuses.includes(m.status ?? "UNKNOWN")); if (matching.length > 0) { tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks); } @@ -359,6 +389,14 @@ : [...tag_activeTags, tag]; } + function tagToggleStatus(status: string) { + tag_srcNextPage = new Map(); + tag_sourceResults = []; + tag_activeStatuses = tag_activeStatuses.includes(status) + ? tag_activeStatuses.filter((s) => s !== status) + : [...tag_activeStatuses, status]; + } + function tagToggleSearchSources() { tag_searchSources = !tag_searchSources; if (tag_searchSources && tag_activeTags.length > 0 && !loadingSources) { @@ -678,13 +716,26 @@ {#if tag_tagFilter} {/if}
+
Status
+ {#each MANGA_STATUSES as { value, label } (value)} + + {/each} + +
Genre
{#each tag_filteredGenres as tag (tag)}
@@ -709,12 +760,18 @@

Browse by tag

-

Select one or more genre tags to find matching manga.

+

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} @@ -752,13 +809,27 @@ Sources - +
- {tag_activeTags.length === 1 ? tag_activeTags[0] : `${tag_activeTags.length} tags (${tag_tagMode})`} + {#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} @@ -834,7 +905,7 @@
{:else}
-

No results for {tag_activeTags.join(` ${tag_tagMode} `)}

+

No results

{#if tag_searchSources} Try OR mode or broader tags. @@ -1429,6 +1500,21 @@ scrollbar-width: thin; scrollbar-color: var(--border-dim) transparent; } + .splitSectionLabel { + font-family: var(--font-ui); + font-size: var(--text-2xs); + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--text-faint); + padding: var(--sp-2) var(--sp-3) var(--sp-1); + pointer-events: none; + user-select: none; + } + .splitSectionLabelSpaced { + margin-top: var(--sp-2); + border-top: 1px solid var(--border-dim); + padding-top: var(--sp-3); + } .splitItem { display: flex; align-items: center; @@ -1547,8 +1633,13 @@ letter-spacing: var(--tracking-wide); color: var(--accent-fg); } + .tagPillStatus { + background: color-mix(in srgb, var(--color-info, #4a90d9) 12%, transparent); + border-color: color-mix(in srgb, var(--color-info, #4a90d9) 30%, transparent); + color: var(--color-info, #4a90d9); + } .tagPillRemove { - color: var(--accent-fg); + color: currentColor; opacity: 0.6; font-size: 13px; line-height: 1; diff --git a/src/components/pages/SeriesDetail.svelte b/src/components/pages/SeriesDetail.svelte index 2930b36..eeaffb6 100644 --- a/src/components/pages/SeriesDetail.svelte +++ b/src/components/pages/SeriesDetail.svelte @@ -1,15 +1,18 @@ {#if store.activeManga}