From 244447da9b72fdab2aab11074591df779f838e74 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Sun, 10 May 2026 04:31:27 -0500 Subject: [PATCH] Feat: Backtracing + NavPage Store --- src/core/util.ts | 25 ++++++++----- .../discover/components/KeywordTab.svelte | 2 +- .../discover/components/Search.svelte | 4 ++- .../library/components/Library.svelte | 14 +++++++- .../series/components/ChapterList.svelte | 4 ++- .../series/components/SeriesDetail.svelte | 16 +++++++++ .../components/TrackingPreview.svelte | 2 +- src/shared/chrome/Sidebar.svelte | 4 ++- src/shared/manga/MangaPreview.svelte | 5 ++- src/store/app.svelte.ts | 36 ++++++++++++------- src/store/state.svelte.ts | 5 +-- 11 files changed, 88 insertions(+), 29 deletions(-) diff --git a/src/core/util.ts b/src/core/util.ts index 967603e..f3d808a 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -31,13 +31,13 @@ export function formatReadTime(m: number): string { const STRICT_TAGS: string[] = [ "adult", "mature", "hentai", "ecchi", "erotic", "pornograph", - "18+", "smut", "lemon", "explicit", "sexual violence", + "18+", "smut", "explicit", "sexual violence", "gore", "guro", "graphic violence", "torture", "body horror", ]; const MODERATE_TAGS: string[] = [ "adult", "mature", "hentai", "ecchi", "erotic", "pornograph", - "18+", "smut", "lemon", "explicit", "sexual violence", + "18+", "smut", "explicit", "sexual violence", ]; type ContentFilterSettings = Pick< @@ -53,7 +53,16 @@ function blockedTagsForSettings(settings: ContentFilterSettings): string[] { function genreMatchesBlocklist(genre: string[], blockedTags: string[]): boolean { if (!blockedTags.length) return false; - return genre.some(g => blockedTags.some(tag => g.toLowerCase().trim().includes(tag))); + return genre.some(g => { + const norm = g.toLowerCase().trim(); + return blockedTags.some(tag => { + const idx = norm.indexOf(tag); + if (idx === -1) return false; + const before = idx === 0 || /\W/.test(norm[idx - 1]); + const after = idx + tag.length === norm.length || /\W/.test(norm[idx + tag.length]); + return before && after; + }); + }); } export function shouldHideNsfw( @@ -69,10 +78,10 @@ export function shouldHideNsfw( if (srcId && blocked.includes(srcId)) return true; const sourceAllowed = !!(srcId && allowed.includes(srcId)); - const blockedTags = blockedTagsForSettings(settings); - if (!sourceAllowed && manga.source?.isNsfw && settings.contentLevel === "strict") return true; - return genreMatchesBlocklist(manga.genre ?? [], blockedTags); + if (!sourceAllowed && manga.source?.isNsfw) return true; + + return genreMatchesBlocklist(manga.genre ?? [], blockedTagsForSettings(settings)); } export function shouldHideSource( @@ -83,10 +92,10 @@ export function shouldHideSource( if (settings.sourceOverridesEnabled) { if ((settings.nsfwBlockedSourceIds ?? []).includes(source.id)) return true; - if ((settings.nsfwAllowedSourceIds ?? []).includes(source.id)) return settings.contentLevel === "strict"; + if ((settings.nsfwAllowedSourceIds ?? []).includes(source.id)) return false; } - return source.isNsfw && settings.contentLevel === "strict"; + return source.isNsfw; } export function dedupeSourcesByLang( diff --git a/src/features/discover/components/KeywordTab.svelte b/src/features/discover/components/KeywordTab.svelte index 21dab40..3a63851 100644 --- a/src/features/discover/components/KeywordTab.svelte +++ b/src/features/discover/components/KeywordTab.svelte @@ -76,7 +76,7 @@ let filtered = allSources; if (kw_selectedLangs.size > 0) filtered = filtered.filter((s) => kw_selectedLangs.has(s.lang)); - if (!store.settings.showNsfw) + if (store.settings.contentLevel !== "unrestricted") filtered = filtered.filter((s) => !shouldHideSource(s, store.settings)); return filtered; } diff --git a/src/features/discover/components/Search.svelte b/src/features/discover/components/Search.svelte index c99aa77..c31dfae 100644 --- a/src/features/discover/components/Search.svelte +++ b/src/features/discover/components/Search.svelte @@ -8,7 +8,7 @@ import { deprioritizeQueue } from "@core/cache/imageCache"; import { dedupeSourcesByLang }from "@core/algorithms/filter"; import { shouldHideNsfw } from "@core/util"; - import { store, setSearchPrefill, setPreviewManga } from "@store/state.svelte"; + import { store, setSearchPrefill, setPreviewManga, setSearchQuery } from "@store/state.svelte"; import { toCachedManga, type CachedManga, @@ -288,6 +288,8 @@ popularResults={popular_results} popularLoading={popular_loading} {sourceCache} + query={store.searchQuery} + onQueryChange={setSearchQuery} onPrefillConsumed={() => (pendingPrefill = "")} onPreview={setPreviewManga} /> diff --git a/src/features/library/components/Library.svelte b/src/features/library/components/Library.svelte index 5d408ba..6122255 100644 --- a/src/features/library/components/Library.svelte +++ b/src/features/library/components/Library.svelte @@ -18,6 +18,7 @@ store, setCategories, setLibraryUpdates, addToast, setTabSort, toggleTabSortDir, setTabStatus, toggleTabFilter, clearTabFilters, } from "../store/libraryState.svelte"; + import { saveScroll, getScroll } from "@store/state.svelte"; import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter, LibraryUpdateEntry } from "@store/state.svelte"; import type { Manga, Category, Chapter } from "@types"; import { checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings } from "@store/state.svelte"; @@ -171,7 +172,18 @@ $effect(() => { filtered; untrack(() => { renderVisible = paginator.reset(); }); }); $effect(() => { retryCount; loading = true; error = null; if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY); untrack(() => loadData()); }); - $effect(() => { if (scrollEl) scrollEl.scrollTo({ top: 0 }); }); + let prevTab = tab; + $effect(() => { + const nextTab = tab; + if (scrollEl && nextTab !== prevTab) { + saveScroll(`library:${prevTab}`, scrollEl.scrollTop); + const saved = getScroll(`library:${nextTab}`); + untrack(() => { scrollEl.scrollTo({ top: saved }); }); + prevTab = nextTab; + } else if (scrollEl && nextTab === prevTab) { + scrollEl.scrollTo({ top: 0 }); + } + }); $effect(() => { const f = tab; if (f === "library" || f === "downloaded") return; diff --git a/src/features/series/components/ChapterList.svelte b/src/features/series/components/ChapterList.svelte index a729bd8..8390008 100644 --- a/src/features/series/components/ChapterList.svelte +++ b/src/features/series/components/ChapterList.svelte @@ -14,6 +14,7 @@ enqueueing: Set; chapterPage: number; totalPages: number; + scrollEl?: HTMLDivElement | null; onOpen: (ch: Chapter, inProgress: boolean) => void; onToggleSelect: (id: number, e: MouseEvent | KeyboardEvent) => void; onEnqueue: (ch: Chapter, e: MouseEvent) => void; @@ -25,6 +26,7 @@ let { pageChapters, sortedChapters, viewMode, loadingChapters, selectedIds, enqueueing, chapterPage, totalPages, + scrollEl = $bindable(null), onOpen, onToggleSelect, onEnqueue, onDeleteDownload, onPageChange, buildCtxItems, }: Props = $props(); @@ -48,7 +50,7 @@ } -
+
{#if loadingChapters && sortedChapters.length === 0} {#if viewMode === "grid"} {#each Array(24) as _}
{/each} diff --git a/src/features/series/components/SeriesDetail.svelte b/src/features/series/components/SeriesDetail.svelte index a6df104..009c35d 100644 --- a/src/features/series/components/SeriesDetail.svelte +++ b/src/features/series/components/SeriesDetail.svelte @@ -17,6 +17,7 @@ addBookmark, acknowledgeUpdate, checkAndMarkCompleted as storeCheckAndMarkCompleted, clearMarkersForManga, + saveScroll, getScroll, } from "@store/state.svelte"; import { trackingState } from "@features/tracking/store/trackingState.svelte"; import type { MangaPrefs } from "@store/state.svelte"; @@ -583,6 +584,20 @@ } catch (e) { console.error(e); } } + let chapterListEl: HTMLDivElement | null = $state(null); + let prevMangaId: number | null = null; + + $effect(() => { + const mangaId = store.activeManga?.id ?? null; + if (mangaId === prevMangaId) return; + if (chapterListEl && prevMangaId !== null) saveScroll(`series:${prevMangaId}`, chapterListEl.scrollTop); + prevMangaId = mangaId; + if (chapterListEl && mangaId !== null) { + const saved = getScroll(`series:${mangaId}`); + chapterListEl.scrollTo({ top: saved }); + } + }); + onMount(() => () => { mangaAbort?.abort(); chapterAbort?.abort(); }); @@ -665,6 +680,7 @@ {enqueueing} {chapterPage} {totalPages} + bind:scrollEl={chapterListEl} onOpen={openReaderWithAhead} onToggleSelect={toggleSelect} onEnqueue={enqueue} diff --git a/src/features/tracking/components/TrackingPreview.svelte b/src/features/tracking/components/TrackingPreview.svelte index d4b9c7f..3615408 100644 --- a/src/features/tracking/components/TrackingPreview.svelte +++ b/src/features/tracking/components/TrackingPreview.svelte @@ -115,7 +115,7 @@ function openManga() { if (!record.manga) return; setActiveManga(record.manga as any); - setNavPage("library"); + setNavPage(store.navPage); onClose(); } diff --git a/src/shared/chrome/Sidebar.svelte b/src/shared/chrome/Sidebar.svelte index 101aeb5..51aaf54 100644 --- a/src/shared/chrome/Sidebar.svelte +++ b/src/shared/chrome/Sidebar.svelte @@ -25,6 +25,7 @@ store.activeManga = null; store.activeSource = null; store.genreFilter = ""; + store.searchQuery = ""; } function goHome() { @@ -33,6 +34,7 @@ store.activeManga = null; store.libraryFilter = "library"; store.genreFilter = ""; + store.searchQuery = ""; } @@ -91,4 +93,4 @@ .settings-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } .settings-btn.anims { transition: color var(--t-base), background var(--t-base), transform var(--t-slow); } .settings-btn.anims:hover { transform: rotate(30deg); } - + \ No newline at end of file diff --git a/src/shared/manga/MangaPreview.svelte b/src/shared/manga/MangaPreview.svelte index 9a15253..00a3d7f 100644 --- a/src/shared/manga/MangaPreview.svelte +++ b/src/shared/manga/MangaPreview.svelte @@ -42,6 +42,8 @@ let loadingLinkList = $state(false); let coverPickerOpen = $state(false); + let originNavPage = store.navPage; + const linkedIds = $derived( store.previewManga ? (store.settings.mangaLinks?.[store.previewManga.id] ?? []) : [], ); @@ -152,6 +154,7 @@ const shouldAutoLink = store.settings.autoLinkOnOpen; const focal = store.previewManga; if (focal) { + originNavPage = store.navPage; load(focal.id); loadCategories(focal.id); if (shouldAutoLink) { @@ -256,7 +259,7 @@ function openSeriesDetail() { if (!displayManga) return; setActiveManga(displayManga); - setNavPage("library"); + setNavPage(originNavPage); close(); } diff --git a/src/store/app.svelte.ts b/src/store/app.svelte.ts index 1edc861..f22bd06 100644 --- a/src/store/app.svelte.ts +++ b/src/store/app.svelte.ts @@ -3,20 +3,32 @@ export type NavPage = | "downloads" | "extensions" | "history" | "search" | "tracking"; class AppStore { - navPage: NavPage = $state("home"); - settingsOpen: boolean = $state(false); - searchPrefill: string = $state(""); - genreFilter: string = $state(""); + navPage: NavPage = $state("home"); + settingsOpen: boolean = $state(false); + searchPrefill: string = $state(""); + searchQuery: string = $state(""); + genreFilter: string = $state(""); + scrollPositions: Map = $state(new Map()); - setNavPage(next: NavPage) { this.navPage = next; } - setSettingsOpen(next: boolean) { this.settingsOpen = next; } - setSearchPrefill(next: string) { this.searchPrefill = next; } - setGenreFilter(next: string) { this.genreFilter = next; } + setNavPage(next: NavPage) { this.navPage = next; } + setSettingsOpen(next: boolean) { this.settingsOpen = next; } + setSearchPrefill(next: string) { this.searchPrefill = next; } + setSearchQuery(next: string) { this.searchQuery = next; } + setGenreFilter(next: string) { this.genreFilter = next; } + saveScroll(key: string, top: number) { + const m = new Map(this.scrollPositions); + m.set(key, top); + this.scrollPositions = m; + } + getScroll(key: string): number { return this.scrollPositions.get(key) ?? 0; } } export const app = new AppStore(); -export function setNavPage(next: NavPage) { app.setNavPage(next); } -export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next); } -export function setSearchPrefill(next: string) { app.setSearchPrefill(next); } -export function setGenreFilter(next: string) { app.setGenreFilter(next); } +export function setNavPage(next: NavPage) { app.setNavPage(next); } +export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next); } +export function setSearchPrefill(next: string) { app.setSearchPrefill(next); } +export function setSearchQuery(next: string) { app.setSearchQuery(next); } +export function setGenreFilter(next: string) { app.setGenreFilter(next); } +export function saveScroll(key: string, top: number) { app.saveScroll(key, top); } +export function getScroll(key: string): number { return app.getScroll(key); } \ No newline at end of file diff --git a/src/store/state.svelte.ts b/src/store/state.svelte.ts index e63cd47..3b9e114 100644 --- a/src/store/state.svelte.ts +++ b/src/store/state.svelte.ts @@ -43,7 +43,6 @@ function mergeSettings(saved: any): Settings { mangaPrefs: saved?.settings?.mangaPrefs ?? {}, customThemes: saved?.settings?.customThemes ?? [], hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [], - nsfwFilteredTags: saved?.settings?.nsfwFilteredTags ?? DEFAULT_SETTINGS.nsfwFilteredTags, nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [], nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [], libraryTabSort: saved?.settings?.libraryTabSort ?? {}, @@ -94,6 +93,8 @@ class Store { set settingsOpen(v) { app.setSettingsOpen(v); } get searchPrefill() { return app.searchPrefill; } set searchPrefill(v) { app.setSearchPrefill(v); } + get searchQuery() { return app.searchQuery; } + set searchQuery(v) { app.setSearchQuery(v); } get genreFilter() { return app.genreFilter; } set genreFilter(v) { app.setGenreFilter(v); } @@ -401,4 +402,4 @@ export async function checkAndMarkCompleted( ): Promise { return store.checkAndMarkCompleted(mangaId, chaps, categories, gqlFn, UPDATE_MANGA_CATEGORIES, UPDATE_MANGA, mangaStatus); } export { addToast, dismissToast, setActiveDownloads } from "./notifications.svelte"; -export { setNavPage, setSettingsOpen, setSearchPrefill, setGenreFilter } from "./app.svelte"; \ No newline at end of file +export { setNavPage, setSettingsOpen, setSearchPrefill, setSearchQuery, setGenreFilter, saveScroll, getScroll } from "./app.svelte"; \ No newline at end of file