From d91ed2e6d1bd58a2b403ae59cb6aa2ee631847d5 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Wed, 1 Apr 2026 15:38:10 -0500 Subject: [PATCH] Chore: Redesign MigrationModal Sources --- src/components/pages/MigrateModal.svelte | 96 +++++++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/src/components/pages/MigrateModal.svelte b/src/components/pages/MigrateModal.svelte index 4d70509..cd34c87 100644 --- a/src/components/pages/MigrateModal.svelte +++ b/src/components/pages/MigrateModal.svelte @@ -3,6 +3,7 @@ import { untrack } from "svelte"; import { gql, thumbUrl } from "../../lib/client"; import { GET_SOURCES, FETCH_SOURCE_MANGA, FETCH_CHAPTERS, UPDATE_MANGA, UPDATE_CHAPTERS_PROGRESS } from "../../lib/queries"; + import { store } from "../../store/state.svelte"; import type { Manga, Source, Chapter } from "../../lib/types"; interface Props { @@ -37,6 +38,46 @@ let sources: Source[] = $state([]); let loadingSources = $state(true); let selectedSource: Source | null = $state(null); + + // Lang filter: "en" first, then alphabetical + let selectedLang: string = $state("all"); + let langStripEl: HTMLDivElement | undefined = $state(); + const availableLangs = $derived.by(() => { + const langs = Array.from(new Set(sources.map(s => s.lang))).sort(); + const en = langs.indexOf("en"); + if (en > 0) { langs.splice(en, 1); langs.unshift("en"); } + return langs; + }); + const hasMultipleLangs = $derived(availableLangs.length > 1); + + function scrollLangStrip(dir: -1 | 1) { + if (!langStripEl) return; + const strip = langStripEl; + const chips = Array.from(strip.children) as HTMLElement[]; + const scrollLeft = strip.scrollLeft; + const viewEnd = scrollLeft + strip.clientWidth; + + if (dir === 1) { + // Find first chip that is cut off or fully outside the right edge, scroll it flush left + const next = chips.find(c => c.offsetLeft + c.offsetWidth > viewEnd + 2); + if (next) strip.scrollTo({ left: next.offsetLeft, behavior: "smooth" }); + } else { + // Find last chip that is cut off or fully outside the left edge, scroll it flush right + const prev = [...chips].reverse().find(c => c.offsetLeft < scrollLeft - 2); + if (prev) strip.scrollTo({ left: prev.offsetLeft + prev.offsetWidth - strip.clientWidth, behavior: "smooth" }); + } + } + const visibleSources = $derived.by(() => { + if (selectedLang !== "all") return sources.filter(s => s.lang === selectedLang); + const map = new Map(); + for (const s of sources) { + const existing = map.get(s.name); + if (!existing) { map.set(s.name, s); continue; } + if (s.lang < existing.lang) map.set(s.name, s); + } + return Array.from(map.values()); + }); + let query = $state(untrack(() => manga.title)); let results: { manga: Manga; similarity: number }[] = $state([]); let searching = $state(false); @@ -52,7 +93,14 @@ $effect(() => { gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) - .then((d) => { sources = d.sources.nodes.filter((s) => s.id !== "0" && s.id !== manga.source?.id); }) + .then((d) => { + const filtered = d.sources.nodes.filter((s) => s.id !== "0" && s.id !== manga.source?.id); + sources = filtered; + // Pre-select preferred lang if available and there are multiple + const prefLang = store?.settings?.preferredExtensionLang ?? ""; + const langs = new Set(filtered.map(s => s.lang)); + if (prefLang && langs.has(prefLang) && langs.size > 1) selectedLang = prefLang; + }) .catch(console.error) .finally(() => { loadingSources = false; }); @@ -178,15 +226,29 @@ {#if step === "source"} -
- {#if loadingSources} -
- + {#if loadingSources} +
+ +
+ {:else if sources.length === 0} +
No other sources installed.
+ {:else} + {#if hasMultipleLangs} +
+ +
+ + {#each availableLangs as lang} + + {/each} +
+
- {:else if sources.length === 0} -
No other sources installed.
- {:else} - {#each sources as src} + {/if} +
+ {#each visibleSources as src} {/each} - {/if} -
+
+ {/if} {:else if step === "search"} @@ -400,6 +462,18 @@ :global(.source-arrow) { color: var(--text-faint); opacity: 0; transition: opacity var(--t-base); } .source-row:hover :global(.source-arrow) { opacity: 1; } + /* Lang filter bar */ + .src-lang-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; } + .src-lang-nav { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex-shrink: 0; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-size: 15px; line-height: 1; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } + .src-lang-nav:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); } + .src-lang-chips { display: flex; align-items: center; gap: var(--sp-1); flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; scroll-behavior: smooth; } + .src-lang-chip:last-child { margin-right: var(--sp-1); } + .src-lang-chips::-webkit-scrollbar { display: none; } + .src-lang-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } + .src-lang-chip:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); } + .src-lang-chip-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); } + .src-lang-chip-active:hover { color: var(--accent-fg); border-color: var(--accent); } + /* Search step */ .search-step { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); } .search-context { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); flex-shrink: 0; }