diff --git a/Todo b/Todo index 9182c81..cd32475 100644 --- a/Todo +++ b/Todo @@ -1,21 +1,18 @@ Major Revisions: - - Moku + Crossplatform Support (MacOS Remaining) - Contemplate Anime Support, Add Novel Support (Consumet API) - - Enable Cloudflare Bypass (Suwayomi Config) - Moku-Share to Easily Migrate/Share Manga (Investigate Usecase/Feasibility) - - Adjustment in Settings for Theme Editor: - - Allow User to Edit/Create Themes - - Allow For Command-Line IPC for Temporary (Apply Once) & Permanent Themes OR External Methodology for Matugen Colors Minor Revisions: - Improve Deduplication Algorithm with Advanced Option Settings (Implement Chapter & Author-based Comparisons, Resource Intensive) - Integrate Download Directory Changes (Settings) - 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: - Cache ALL Cover Pictures & Details for Manga in Library - - MacOS Full-Screen & UI Compatability (TitleBar) + - Investigate Zoom (Reader), Appears to have Cutoff, etc. General/Misc Bugs: - Fix Highlightable Elements @@ -25,9 +22,12 @@ General/Misc Bugs: - Investigate Prod vs. Dev Directory Change/Data Load (Caused by Library Algorithm Revision?) In-Progress:` - - Fix Reader Chapter Shifts (Glitched Sentinel) - - Still Shifts Down after reading ~8+ Chapters? - - Identify When Chapters are Unloaded, How to Preserve Structure + - Enable Cloudflare Bypass (Suwayomi Config) (Requires Patching) + - Fix NSFW Parsing (Appears to not Work???) + + - Adjustment in Settings for Theme Editor: + - Patch Color-Picker to Work Properly + Important Commands: diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b761fed..4b4a6ab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -248,7 +248,28 @@ struct ServerInvocation { working_dir: Option, } +/// Locate the `java` / `java.exe` binary inside a bundled JRE directory. +/// +/// Expected layout (Windows and Linux): +/// /jre/bin/java[.exe] +/// +/// On Windows `strip_unc` is applied so Java doesn't choke on `\\?\` prefixes. +#[cfg(not(target_os = "macos"))] +fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option) -> Option { + #[cfg(target_os = "windows")] + let java = strip_unc(bundle_dir.join("jre").join("bin").join("java.exe")); + + #[cfg(not(target_os = "windows"))] + let java = bundle_dir.join("jre").join("bin").join("java"); + + do_log(log, &format!("[find_java] checking path: {:?}", java)); + do_log(log, &format!("[find_java] exists: {}", java.exists())); + + if java.exists() { Some(java) } else { None } +} + fn do_log(log: &mut Option, msg: &str) { + eprintln!("{}", msg); if let Some(f) = log { let _ = writeln!(f, "{}", msg); } @@ -261,7 +282,10 @@ fn resolve_server_binary( ) -> Result { do_log(log, &format!("[resolve] binary arg = {:?}", binary)); - // 1. User-specified binary path + // ── 1. User-specified binary path ───────────────────────────────────────── + // Primary: honour the path as-is (doc-2 behaviour — trust the user). + // Fallback: if the path doesn't exist after stripping UNC, log a warning + // and continue so the bundled detection still has a chance. if !binary.trim().is_empty() { let path = strip_unc(PathBuf::from(binary.trim())); do_log(log, &format!("[resolve] user path: {:?} exists={}", path, path.exists())); @@ -272,17 +296,58 @@ fn resolve_server_binary( working_dir: path.parent().map(|p| p.to_path_buf()), }); } - return Err(SpawnError::NotConfigured( - format!("Configured binary not found: {}", path.display()), - )); + // Fallback: path was set but file is missing — warn and keep trying. + do_log(log, "[resolve] WARNING: user-supplied path not found, falling through to bundled detection"); } - // 2. Bundled sidecar (Windows / Linux AppImage) + // Resolve and UNC-strip resource_dir once; used by all non-macOS branches. + #[cfg(not(target_os = "macos"))] + let resource_dir = { + let raw = app.path().resource_dir().unwrap_or_default(); + let stripped = strip_unc(raw); + do_log(log, &format!("[resolve] resource_dir (stripped) = {:?}", stripped)); + stripped + }; + + // ── 2. Bundled JRE + JAR (Windows / Linux — specific layout) ───────────── + // Primary path from doc-2: binaries/suwayomi-bundle/bin/Suwayomi-Server.jar #[cfg(not(target_os = "macos"))] { - let resource_dir = app.path().resource_dir().unwrap_or_default(); - let candidates = ["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"]; - for name in &candidates { + let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle"); + let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); + + do_log(log, &format!("[resolve] bundle_dir = {:?}", bundle_dir)); + do_log(log, &format!("[resolve] bundle_dir exists: {}", bundle_dir.exists())); + do_log(log, &format!("[resolve] jar = {:?}", jar)); + do_log(log, &format!("[resolve] jar exists: {}", jar.exists())); + + match find_java_in_bundle(&bundle_dir, log) { + Some(java) => { + do_log(log, &format!("[resolve] java found: {:?}", java)); + if jar.exists() { + do_log(log, "[resolve] both java and jar found — using bundled JRE"); + return Ok(ServerInvocation { + bin: java.to_string_lossy().into_owned(), + args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()], + working_dir: Some(bundle_dir), + }); + } + do_log(log, "[resolve] java found but jar MISSING — falling through"); + } + None => { + do_log(log, "[resolve] java NOT found in bundle — falling through"); + } + } + } + + // ── 2b. Bundled launcher scripts / native sidecars (Windows / Linux) ────── + // Fallback for older bundle layouts that ship a wrapper script instead of a + // bare JRE + JAR. Also scans for any *.jar in resource_dir as a last resort. + #[cfg(not(target_os = "macos"))] + { + // Named launcher scripts. + let script_candidates = ["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"]; + for name in &script_candidates { let p = resource_dir.join(name); do_log(log, &format!("[resolve] sidecar candidate: {:?} exists={}", p, p.exists())); if p.exists() { @@ -290,24 +355,64 @@ fn resolve_server_binary( return Ok(ServerInvocation { bin: p.to_string_lossy().into_owned(), args: vec![], - working_dir: Some(resource_dir), + working_dir: Some(resource_dir.clone()), }); } } + + // Generic JRE at resource_dir root + any *.jar alongside it. + do_log(log, "[resolve] no named sidecar found, trying generic JRE + any jar in resource_dir"); + if let Some(java) = find_java_in_bundle(&resource_dir, log) { + let jar = std::fs::read_dir(&resource_dir) + .ok() + .and_then(|mut rd| { + rd.find(|e| { + e.as_ref() + .map(|e| e.file_name().to_string_lossy().ends_with(".jar")) + .unwrap_or(false) + }) + .and_then(|e| e.ok()) + .map(|e| e.path()) + }); + + do_log(log, &format!("[resolve] generic jar candidate: {:?}", jar)); + + if let Some(jar_path) = jar { + do_log(log, &format!("[resolve] using generic JRE java={:?} jar={:?}", java, jar_path)); + return Ok(ServerInvocation { + bin: java.to_string_lossy().into_owned(), + args: vec!["-jar".to_string(), jar_path.to_string_lossy().into_owned()], + working_dir: Some(resource_dir), + }); + } + do_log(log, "[resolve] generic JRE found but no .jar — falling through"); + } } - // 3. macOS app bundle — look in MacOS/ and Resources/ + // ── 3. macOS app bundle — MacOS/ then Resources/ ────────────────────────── #[cfg(target_os = "macos")] { let resource_dir = app.path().resource_dir().unwrap_or_default(); - let macos_dir = resource_dir.parent() + let macos_dir = resource_dir + .parent() .map(|p| p.join("MacOS")) .unwrap_or_default(); - let candidates = ["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"]; + do_log(log, &format!("[resolve] macOS macos_dir = {:?}", macos_dir)); + + // Tauri strips the target triple when installing externalBin sidecars into + // Contents/MacOS/, so the binary is "suwayomi-server" at runtime. + // Triple-suffixed names are kept as a belt-and-suspenders fallback for + // dev / flat layouts. + let candidates = [ + "suwayomi-server", + "suwayomi-server-aarch64-apple-darwin", + "suwayomi-server-x86_64-apple-darwin", + "suwayomi-launcher", + "suwayomi-launcher.sh", + "tachidesk-server", + ]; - // Search MacOS/ first (correct location), then Resources/ as fallback - // for flat dev layouts where the script sits next to resources. for search_dir in &[&macos_dir, &resource_dir] { for name in &candidates { let p = search_dir.join(name); @@ -324,8 +429,18 @@ fn resolve_server_binary( } } + // ── 4. PATH fallback ────────────────────────────────────────────────────── + // Use `where` on Windows, `which` everywhere else. do_log(log, "[resolve] trying PATH fallback"); for name in &["suwayomi-server", "tachidesk-server"] { + #[cfg(target_os = "windows")] + let found = std::process::Command::new("where") + .arg(name) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + + #[cfg(not(target_os = "windows"))] let found = std::process::Command::new("which") .arg(name) .output() diff --git a/src/App.svelte b/src/App.svelte index 8badeac..1422f7a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -76,21 +76,14 @@ let idle = $state(false); let devSplash = $state(false); - // The OS/monitor DPI scale factor for the current display. - // Queried from Rust (window.scale_factor()) on mount and updated live - // whenever the window moves to a different monitor via the scaleChanged event. - // 1.0 = standard display, 2.0 = HiDPI/4K, 1.25–1.5 = Windows scaled display. let platformScale = $state(1.0); - // effectiveZoom = platformScale × uiZoom (user preference, float, default 1.0) - // Applied to document.documentElement so the entire UI scales correctly. function applyZoom() { const uiZoom = store.settings.uiZoom ?? 1.5; const effective = platformScale * uiZoom; const pct = effective * 100; document.documentElement.style.zoom = `${pct}%`; document.documentElement.style.setProperty("--ui-scale", String(effective)); - // visual-vh compensates for the zoom so 100vh-based calculations stay correct. document.documentElement.style.setProperty("--visual-vh", `${window.innerHeight / effective}px`); } @@ -136,7 +129,6 @@ return () => idleEvents.forEach(e => window.removeEventListener(e, resetIdle)); }); - // Re-apply zoom whenever uiZoom setting or platformScale changes. $effect(() => { store.settings.uiZoom; platformScale; applyZoom(); @@ -226,8 +218,6 @@ document.addEventListener("contextmenu", e => e.preventDefault()); (window as any).__mokuShowSplash = () => devSplash = true; - // Fetch the real monitor scale factor from Rust (window.scale_factor()). - // This reflects actual DPI — 2.0 on HiDPI, 1.25 on Windows scaled displays, etc. platformScale = await invoke("get_platform_ui_scale").catch(() => 1.0); applyZoom(); @@ -237,9 +227,6 @@ store.isFullscreen = await win.isFullscreen(); }); - // Re-query the scale factor when the window moves to a different monitor. - // Tauri emits this event whenever the DPI changes (e.g. dragging window - // from a 1080p display to a 4K display). const unlistenScale = await win.onScaleChanged(async (event) => { platformScale = event.payload.scaleFactor; applyZoom(); @@ -288,7 +275,6 @@ } }); - // When the reader closes, show idle presence. $effect(() => { if (!store.activeChapter) { diff --git a/src/components/pages/Discover.svelte b/src/components/pages/Discover.svelte index 48825c5..0337f82 100644 --- a/src/components/pages/Discover.svelte +++ b/src/components/pages/Discover.svelte @@ -4,7 +4,7 @@ import { gql, thumbUrl } from "../../lib/client"; import { GET_SOURCES, FETCH_SOURCE_MANGA, UPDATE_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries"; import { cache, CACHE_KEYS } from "../../lib/cache"; - import { dedupeSources, dedupeMangaByTitle, dedupeMangaById, isNsfwManga } from "../../lib/util"; + import { dedupeSources, dedupeMangaByTitle, dedupeMangaById, shouldHideNsfw } from "../../lib/util"; import { store, setPreviewManga, clearDiscoverCache } from "../../store/state.svelte"; import type { Manga, Source, Category } from "../../lib/types"; import ContextMenu from "../shared/ContextMenu.svelte"; @@ -62,7 +62,7 @@ function filterOut(mangas: Manga[]): Manga[] { return dedup(mangas.filter(m => { if (m.inLibrary || store.discoverLibraryIds.has(m.id)) return false; - if (!store.settings.showNsfw && isNsfwManga(m)) return false; + if (shouldHideNsfw(m, store.settings)) return false; return true; })); } @@ -188,7 +188,7 @@ if (ctrl.signal.aborted) return; const local = dedup( - d.mangas.nodes.filter(m => store.settings.showNsfw || !isNsfwManga(m)) + d.mangas.nodes.filter(m => !shouldHideNsfw(m, store.settings)) ); store.discoverCache.set(localKey, local); genreResults.set(genre, local.slice(0, GRID_LIMIT)); diff --git a/src/components/pages/Library.svelte b/src/components/pages/Library.svelte index cac101f..1c0ebcc 100644 --- a/src/components/pages/Library.svelte +++ b/src/components/pages/Library.svelte @@ -4,7 +4,7 @@ import { gql, thumbUrl } from "../../lib/client"; import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER } from "../../lib/queries"; import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache"; - import { dedupeMangaById, dedupeMangaByTitle, isNsfwManga } from "../../lib/util"; + import { dedupeMangaById, dedupeMangaByTitle, shouldHideNsfw } from "../../lib/util"; import { store, setLibraryFilter, checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings, setCategories } from "../../store/state.svelte"; import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter } from "../../store/state.svelte"; import type { Manga, Category, Chapter } from "../../lib/types"; @@ -320,9 +320,7 @@ } // 2. NSFW filter — always applied before text search or sort - if (!store.settings.showNsfw) { - items = items.filter(m => !isNsfwManga(m)); - } + items = items.filter(m => !shouldHideNsfw(m, store.settings)); // 3. Text search if (q) items = items.filter(m => m.title.toLowerCase().includes(q)); diff --git a/src/components/pages/Search.svelte b/src/components/pages/Search.svelte index 5360814..06d11ea 100644 --- a/src/components/pages/Search.svelte +++ b/src/components/pages/Search.svelte @@ -3,7 +3,7 @@ import { gql, thumbUrl } from "../../lib/client"; import { GET_SOURCES, FETCH_SOURCE_MANGA } from "../../lib/queries"; import { cache, CACHE_KEYS, getPageSet } from "../../lib/cache"; - import { dedupeSources, dedupeMangaById, dedupeMangaByTitle, isNsfwManga } from "../../lib/util"; + import { dedupeSources, dedupeMangaById, dedupeMangaByTitle, shouldHideNsfw } from "../../lib/util"; import { store, setSearchPrefill, setPreviewManga } from "../../store/state.svelte"; import type { Manga, Source } from "../../lib/types"; @@ -91,14 +91,11 @@ const availableLangs = $derived(Array.from(new Set(allSources.map((s) => s.lang))).sort()); const hasMultipleLangs = $derived(availableLangs.length > 1); - // ── Keyword search ──────────────────────────────────────────────────────── - let kw_query = $state(""); let kw_submitted = $state(""); let kw_results: SourceResult[] = $state([]); let kw_showAdvanced = $state(false); let kw_selectedLangs: Set = $state(new Set()); - let kw_includeNsfw = $state(false); let kw_inputEl: HTMLInputElement | null = $state(null); let kw_abortCtrl: AbortController | null = null; @@ -124,7 +121,7 @@ let filtered = allSources; if (kw_selectedLangs.size > 0) filtered = filtered.filter((s) => kw_selectedLangs.has(s.lang)); - if (!kw_includeNsfw) + if (!store.settings.showNsfw) filtered = filtered.filter((s) => !s.isNsfw); return filtered; } @@ -146,9 +143,7 @@ FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page: 1, query: trimmed }, ctrl.signal, ); if (ctrl.signal.aborted) return; - const mangas = store.settings.showNsfw - ? d.fetchSourceManga.mangas - : d.fetchSourceManga.mangas.filter((m) => !isNsfwManga(m)); + const mangas = d.fetchSourceManga.mangas.filter((m) => !shouldHideNsfw(m, store.settings)); kw_results = kw_results.map((r) => r.source.id === src.id ? { ...r, mangas, loading: false } : r, ); @@ -172,8 +167,6 @@ const kw_hasResults = $derived(kw_results.some((r) => r.mangas.length > 0)); const kw_allDone = $derived(kw_results.length > 0 && kw_results.every((r) => !r.loading)); - // ── Tag search ──────────────────────────────────────────────────────────── - let tag_activeTags: string[] = $state([]); let tag_tagMode: TagMode = $state("AND"); let tag_tagFilter = $state(""); @@ -246,7 +239,7 @@ ctrl.signal, ).then((d) => { if (ctrl.signal.aborted) return; - const nsfwFilter = (m: Manga) => store.settings.showNsfw || !isNsfwManga(m); + const nsfwFilter = (m: Manga) => !shouldHideNsfw(m, store.settings); tag_localResults = d.mangas.nodes.filter(nsfwFilter); tag_totalCount = d.mangas.totalCount; tag_localHasNext = d.mangas.pageInfo.hasNextPage; @@ -286,7 +279,7 @@ const matching = (activeTags.length > 1 ? result.mangas.filter((m) => matchesAllTags(m, activeTags)) : result.mangas - ).filter((m) => store.settings.showNsfw || !isNsfwManga(m)); + ).filter((m) => !shouldHideNsfw(m, store.settings)); if (matching.length > 0) { tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks); tag_loadingSourceSearch = false; @@ -309,8 +302,7 @@ ctrl.signal, ); if (ctrl.signal.aborted) return; - const nsfwFilter = (m: Manga) => store.settings.showNsfw || !isNsfwManga(m); - tag_localResults = [...tag_localResults, ...d.mangas.nodes.filter(nsfwFilter)]; + const nsfwFilter = (m: Manga) => !shouldHideNsfw(m, store.settings); tag_localHasNext = d.mangas.pageInfo.hasNextPage; tag_localOffset += (store.settings.renderLimit ?? 48); } catch (e: any) { @@ -349,7 +341,7 @@ const matching = (tag_activeTags.length > 1 ? result.mangas.filter((m) => matchesAllTags(m, tag_activeTags)) : result.mangas - ).filter((m) => store.settings.showNsfw || !isNsfwManga(m)); + ).filter((m) => !shouldHideNsfw(m, store.settings)); if (matching.length > 0) { tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks); } @@ -374,9 +366,7 @@ } } - // ── Source browse ───────────────────────────────────────────────────────── - - let src_selectedLang = $state("all"); + let src_selectedLang = $state(preferredLang || "all"); let src_activeSource: Source | null = $state(null); let src_browseResults: Manga[] = $state([]); let src_loadingBrowse = $state(false); @@ -385,40 +375,33 @@ let src_hasNextPage = $state(false); let src_currentPage = $state(1); let src_abortCtrl: AbortController | null = null; - let src_langPocketOpen = $state(true); - let src_expandedGroups: Set = $state(new Set()); - // Group sources by displayName — sources with same name but different langs get grouped - interface SourceGroup { - name: string; - iconUrl: string; - sources: Source[]; - isNsfw: boolean; - } + $effect(() => { + if (!allSources.length) return; + const langs = new Set(allSources.map((s) => s.lang)); + if (src_selectedLang !== "all" && !langs.has(src_selectedLang)) { + src_selectedLang = langs.has(preferredLang) ? preferredLang : "all"; + } + }); - const src_visibleSources = $derived(src_selectedLang === "all" - ? allSources - : allSources.filter((s) => s.lang === src_selectedLang)); - - const src_groupedSources = $derived.by(() => { - const filtered = src_visibleSources; - const map = new Map(); - for (const src of filtered) { - const key = src.displayName; - if (!map.has(key)) { - map.set(key, { name: src.displayName, iconUrl: src.iconUrl, sources: [], isNsfw: src.isNsfw }); + const src_visibleSources = $derived.by(() => { + const nsfw = (s: Source) => !store.settings.showNsfw && s.isNsfw; + if (src_selectedLang !== "all") { + return allSources.filter((s) => s.lang === src_selectedLang && !nsfw(s)); + } + const map = new Map(); + for (const s of allSources) { + if (nsfw(s)) continue; + const key = s.name; + const existing = map.get(key); + if (!existing) { map.set(key, s); continue; } + if (s.lang === preferredLang || (!existing || (existing.lang !== preferredLang && s.lang < existing.lang))) { + map.set(key, s); } - map.get(key)!.sources.push(src); } return Array.from(map.values()); }); - function srcToggleGroup(name: string) { - const next = new Set(src_expandedGroups); - if (next.has(name)) next.delete(name); else next.add(name); - src_expandedGroups = next; - } - async function srcFetchBrowse(src: Source, type: "POPULAR" | "SEARCH", q?: string, page = 1) { src_abortCtrl?.abort(); const ctrl = new AbortController(); @@ -429,9 +412,7 @@ FETCH_SOURCE_MANGA, { source: src.id, type, page, query: q ?? null }, ctrl.signal, ); if (ctrl.signal.aborted) return; - const incoming = store.settings.showNsfw - ? d.fetchSourceManga.mangas - : d.fetchSourceManga.mangas.filter((m) => !isNsfwManga(m)); + const incoming = d.fetchSourceManga.mangas.filter((m) => !shouldHideNsfw(m, store.settings)); src_browseResults = page === 1 ? incoming : [...src_browseResults, ...incoming]; src_hasNextPage = d.fetchSourceManga.hasNextPage; src_currentPage = page; @@ -580,10 +561,6 @@ {/each}
-
Searching {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""}
@@ -877,27 +854,18 @@
- - {#if src_langPocketOpen} -
- {#each ["all", ...availableLangs] as lang (lang)} - - {/each} -
- {/if} +
+ Language + +
{#if loadingSources}
@@ -907,52 +875,22 @@
{:else}
- {#each src_groupedSources as group (group.name)} - {#if group.sources.length === 1} - - {:else} - - {#if src_expandedGroups.has(group.name)} - {#each group.sources as src (src.id)} - - {/each} + {#each src_visibleSources as src (src.id)} + {/each} - {#if src_groupedSources.length === 0} + {#if src_visibleSources.length === 0}

No sources for this language

{/if}
@@ -1071,8 +1009,6 @@
- -
- {:else if tab === "security"}
- {#if secError}
{secError}
{/if} -

Server Authentication

@@ -1471,7 +1384,6 @@
-

App Lock

@@ -1505,7 +1417,6 @@
{/if}
-

SOCKS Proxy

@@ -1515,7 +1426,7 @@ Enable SOCKS proxy Route Suwayomi traffic through a SOCKS4/5 proxy
- + {#if socksEnabled}
@@ -1579,7 +1490,6 @@
{/if}
-

FlareSolverr

@@ -1589,7 +1499,7 @@ Enable FlareSolverr Bypass Cloudflare challenges for sources that require it
- + {#if flareEnabled}
@@ -1643,13 +1553,92 @@
{/if}
- - + {:else if tab === "content"} +
+
+

Content Filter

+ +
+
+

Blocked Genre Tags

+

+ Manga whose genres contain any of these substrings are filtered out. Matching is case-insensitive and partial — "erotic" catches "Erotica", "Erotic Content", etc. +

+ + +
+
+

Source Overrides

+

+ Allow lets a source through even if it's flagged NSFW (genre tags still apply). Block always hides a source regardless of the global setting. +

+
+ +
+ {#if contentSourcesLoading} +

Loading sources…

+ {:else if contentSources.length === 0} +

No sources found — check your server connection.

+ {:else} +
+ {#each contentSourcesFiltered as group (group.name)} + {@const ids = group.sources.map(s => s.id)} + {@const allowed = store.settings.nsfwAllowedSourceIds ?? []} + {@const blocked = store.settings.nsfwBlockedSourceIds ?? []} + {@const isAllowed = ids.every(id => allowed.includes(id))} + {@const isBlocked = ids.every(id => blocked.includes(id))} +
+ +
+ {group.name} + {group.sources[0].isNsfw ? "NSFW · " : ""}{group.sources.length > 1 ? `${group.sources.length} languages` : group.sources[0].lang.toUpperCase()} +
+
+ + +
+
+ {/each} +
+ {/if} +
+
{:else if tab === "about"}
- -

Moku

@@ -1657,8 +1646,6 @@

Built with Tauri + Svelte.

- -

Version

@@ -1676,8 +1663,6 @@ ✓ You're on the latest version.
{/if} - - {#if updatePhase === "downloading" && IS_WINDOWS}
@@ -1690,8 +1675,6 @@
{/if} - - {#if updatePhase === "ready"}
@@ -1701,8 +1684,6 @@
{/if} - - {#if updatePhase === "error"}
{updateError} @@ -1710,11 +1691,8 @@
{/if}
- -

Releases

- {#if releasesError}

{releasesError}

{:else if releasesLoading} @@ -1728,9 +1706,7 @@ {@const isExpanded = expandedTag === release.tag_name} {@const isTarget = targetTag === release.tag_name} {@const isInstalling = isTarget && updatePhase === "downloading"} -
-
{release.tag_name} @@ -1742,14 +1718,12 @@ {/if}
- {#if release.body.trim()} {/if} - {#if !isCurrent} {#if IS_WINDOWS}
- - {#if isExpanded && release.body.trim()}
{release.body.trim()}
@@ -1779,8 +1751,6 @@
{/if}
- -

Links

@@ -1788,10 +1758,7 @@ Discord →
-
- - {:else if tab === "devtools"}
@@ -1812,17 +1779,14 @@
{/if} -
- - + .content-tag-grid { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); } + .content-tag { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 4px 8px 4px 7px; border-radius: var(--radius-full); border: 1px solid var(--border-base); background: var(--bg-raised); color: var(--text-secondary); } + .content-tag-remove { display: flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 50%; border: none; background: none; color: var(--text-faint); cursor: pointer; font-size: 14px; line-height: 1; padding: 0; margin-left: 1px; transition: color var(--t-fast), background var(--t-fast); } + .content-tag-remove:hover { color: var(--color-error); background: var(--color-error-bg); } + .content-tag-add { display: flex; align-items: center; gap: var(--sp-2); padding: 0 var(--sp-3); } + .content-source-search-wrap { padding: 0 var(--sp-3) var(--sp-3); } + .content-source-list { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--sp-2); } + .content-source-row { display: flex; align-items: center; gap: var(--sp-3); padding: 8px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; transition: background var(--t-fast), border-color var(--t-fast); } + .content-source-row:hover { background: var(--bg-raised); } + .content-source-allowed { background: color-mix(in srgb, var(--color-success) 6%, transparent); border-color: color-mix(in srgb, var(--color-success) 25%, transparent) !important; } + .content-source-blocked { background: color-mix(in srgb, var(--color-error) 6%, transparent); border-color: color-mix(in srgb, var(--color-error) 25%, transparent) !important; } + .content-source-icon { width: 28px; height: 28px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; border: 1px solid var(--border-dim); background: var(--bg-overlay); } + .content-source-info { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; } + .content-source-name { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .content-source-lang { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); } + .content-source-actions { display: flex; gap: var(--sp-1); flex-shrink: 0; } + .content-action-btn { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } + .content-action-btn:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-overlay); } + .content-action-active-allow { color: var(--color-success) !important; border-color: color-mix(in srgb, var(--color-success) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success) 10%, transparent) !important; } + .content-action-active-block { color: var(--color-error) !important; border-color: color-mix(in srgb, var(--color-error) 40%, transparent) !important; background: color-mix(in srgb, var(--color-error) 10%, transparent) !important; } + \ No newline at end of file diff --git a/src/components/shared/MangaPreview.svelte b/src/components/shared/MangaPreview.svelte index 60ac668..ca8b9cd 100644 --- a/src/components/shared/MangaPreview.svelte +++ b/src/components/shared/MangaPreview.svelte @@ -356,7 +356,7 @@
{/if} {#if continueChapter} - {/if} diff --git a/src/components/shared/SourceList.svelte b/src/components/shared/SourceList.svelte index 0c5fc10..403b03e 100644 --- a/src/components/shared/SourceList.svelte +++ b/src/components/shared/SourceList.svelte @@ -1,4 +1,5 @@