mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Local-Source Popular Query + App-Pin Flow
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
||||||
import logoUrl from '$lib/assets/moku-icon-splash.svg'
|
import logoUrl from '$lib/assets/moku-icon-splash.svg'
|
||||||
|
import { platformService } from '$lib/platform-service'
|
||||||
|
|
||||||
|
const isTauri = platformService.platform === 'tauri'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode?: 'loading' | 'idle' | 'locked'
|
mode?: 'loading' | 'idle' | 'locked'
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (mode === 'loading' && !failed && !notConfigured && !ringFull) {
|
if (mode === 'loading' && !failed && !notConfigured && !ringFull) {
|
||||||
|
if (!isTauri) return // no ring animation on web; probe outcome drives exit
|
||||||
animStart = null
|
animStart = null
|
||||||
animPhase = 1
|
animPhase = 1
|
||||||
animFrame = requestAnimationFrame(animateRing)
|
animFrame = requestAnimationFrame(animateRing)
|
||||||
@@ -278,28 +281,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mountCanvas(el: HTMLCanvasElement) {
|
function mountCanvas(el: HTMLCanvasElement) {
|
||||||
const win = getCurrentWindow()
|
|
||||||
const ctx = el.getContext('2d')!
|
const ctx = el.getContext('2d')!
|
||||||
let live: RenderState | null = null
|
let live: RenderState | null = null
|
||||||
let lastLogW = 0, lastLogH = 0, lastScale = 0, buildGen = 0
|
let lastLogW = 0, lastLogH = 0, lastScale = 0, buildGen = 0
|
||||||
|
|
||||||
async function syncSize() {
|
function applySize(logW: number, logH: number, scale: number) {
|
||||||
const gen = ++buildGen
|
const gen = ++buildGen
|
||||||
const [phys, scale] = await Promise.all([win.innerSize(), win.scaleFactor()])
|
if (logW <= 0 || logH <= 0) return
|
||||||
if (gen !== buildGen) return
|
|
||||||
const logW = phys.width / scale, logH = phys.height / scale
|
|
||||||
if (logW === lastLogW && logH === lastLogH && scale === lastScale) return
|
if (logW === lastLogW && logH === lastLogH && scale === lastScale) return
|
||||||
lastLogW = logW; lastLogH = logH; lastScale = scale
|
lastLogW = logW; lastLogH = logH; lastScale = scale
|
||||||
const built = buildCards(logW, logH)
|
const built = buildCards(logW, logH)
|
||||||
const stamps = built.cards.map(c => buildStamp(c, scale))
|
const stamps = built.cards.map(c => buildStamp(c, scale))
|
||||||
const vig = buildVignette(logW, logH, scale)
|
const vig = buildVignette(logW, logH, scale)
|
||||||
el.width = phys.width; el.height = phys.height
|
el.width = Math.round(logW * scale)
|
||||||
live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: phys.width, CH: phys.height, scale }
|
el.height = Math.round(logH * scale)
|
||||||
|
if (gen === buildGen) live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: el.width, CH: el.height, scale }
|
||||||
}
|
}
|
||||||
|
|
||||||
const ro = new ResizeObserver(() => syncSize())
|
let extraCleanup: (() => void) | undefined
|
||||||
|
|
||||||
|
if (isTauri) {
|
||||||
|
let tauriRo: ResizeObserver | undefined
|
||||||
|
let tauriUnlisten: (() => void) | undefined
|
||||||
|
import('@tauri-apps/api/window').then(({ getCurrentWindow }) => {
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
const doSync = () => Promise.all([win.innerSize(), win.scaleFactor()])
|
||||||
|
.then(([phys, scale]) => applySize(phys.width / scale, phys.height / scale, scale))
|
||||||
|
doSync()
|
||||||
|
tauriRo = new ResizeObserver(() => doSync())
|
||||||
|
tauriRo.observe(el)
|
||||||
|
win.onFocusChanged(() => doSync()).then(u => { tauriUnlisten = u })
|
||||||
|
})
|
||||||
|
extraCleanup = () => { tauriRo?.disconnect(); tauriUnlisten?.() }
|
||||||
|
} else {
|
||||||
|
const syncWeb = () => applySize(el.clientWidth, el.clientHeight, window.devicePixelRatio || 1)
|
||||||
|
const ro = new ResizeObserver(() => syncWeb())
|
||||||
ro.observe(el)
|
ro.observe(el)
|
||||||
syncSize()
|
requestAnimationFrame(() => syncWeb())
|
||||||
|
extraCleanup = () => ro.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
let raf = 0, t0 = -1, paused = false
|
let raf = 0, t0 = -1, paused = false
|
||||||
|
|
||||||
@@ -307,9 +327,11 @@
|
|||||||
if (paused) { raf = 0; return }
|
if (paused) { raf = 0; return }
|
||||||
raf = requestAnimationFrame(frame)
|
raf = requestAnimationFrame(frame)
|
||||||
if (!live) return
|
if (!live) return
|
||||||
|
const { cards, trigs, stamps, vignette, CW, CH, scale } = live
|
||||||
|
if (CW <= 0 || CH <= 0 || vignette.width <= 0 || vignette.height <= 0) return
|
||||||
|
if (stamps.some(s => s.width <= 0 || s.height <= 0)) return
|
||||||
if (t0 < 0) t0 = now
|
if (t0 < 0) t0 = now
|
||||||
if (showFps) tickFps(now)
|
if (showFps) tickFps(now)
|
||||||
const { cards, trigs, stamps, vignette, CW, CH, scale } = live
|
|
||||||
drawFrame(ctx, (now - t0) / 1000, CW, CH, scale, cards, trigs, stamps, vignette)
|
drawFrame(ctx, (now - t0) / 1000, CW, CH, scale, cards, trigs, stamps, vignette)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,14 +340,11 @@
|
|||||||
function onVis() { document.hidden ? pause() : resume() }
|
function onVis() { document.hidden ? pause() : resume() }
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVis)
|
document.addEventListener('visibilitychange', onVis)
|
||||||
const unlistenFocus = win.onFocusChanged(({ payload: focused }) => { focused ? resume() : pause() })
|
|
||||||
|
|
||||||
raf = requestAnimationFrame(frame)
|
raf = requestAnimationFrame(frame)
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf)
|
cancelAnimationFrame(raf)
|
||||||
ro.disconnect()
|
extraCleanup?.()
|
||||||
document.removeEventListener('visibilitychange', onVis)
|
document.removeEventListener('visibilitychange', onVis)
|
||||||
unlistenFocus.then(f => f())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ArrowLeft, MagnifyingGlass, GearSix, Swap, Funnel, Check } from "phosphor-svelte";
|
import { ArrowLeft, MagnifyingGlass, GearSix, Swap, Funnel, Check, CircleNotch } from "phosphor-svelte";
|
||||||
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
|
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
|
||||||
import { resolvedCover } from "$lib/core/cover/coverResolver";
|
import { resolvedCover } from "$lib/core/cover/coverResolver";
|
||||||
import { getAdapter } from "$lib/request-manager";
|
import { getAdapter } from "$lib/request-manager";
|
||||||
@@ -25,9 +25,22 @@
|
|||||||
|
|
||||||
let { pkgName, extensionName, iconUrl, cols, cropCovers, statsAlways, anims, sources, onBack, onSettings }: Props = $props();
|
let { pkgName, extensionName, iconUrl, cols, cropCovers, statsAlways, anims, sources, onBack, onSettings }: Props = $props();
|
||||||
|
|
||||||
|
const isLocal = pkgName === '__local__';
|
||||||
|
|
||||||
|
// ── Library mode state ──────────────────────────────────────────────
|
||||||
let groups: SourceLibrary[] = $state([]);
|
let groups: SourceLibrary[] = $state([]);
|
||||||
|
let sourceNodes: SourceNode[] = $state([]);
|
||||||
|
|
||||||
|
// ── Local/browse mode state ──────────────────────────────────────────
|
||||||
|
let localItems: any[] = $state([]);
|
||||||
|
let localPage: number = $state(1);
|
||||||
|
let localHasNext: boolean = $state(false);
|
||||||
|
let localLoadingMore: boolean = $state(false);
|
||||||
|
|
||||||
|
// ── Shared state ─────────────────────────────────────────────────────
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let search = $state("");
|
let search = $state("");
|
||||||
|
let searchInput = $state("");
|
||||||
|
|
||||||
type ContentFilter = "unread" | "downloaded";
|
type ContentFilter = "unread" | "downloaded";
|
||||||
let activeFilters = $state<Partial<Record<ContentFilter, boolean>>>({});
|
let activeFilters = $state<Partial<Record<ContentFilter, boolean>>>({});
|
||||||
@@ -37,35 +50,80 @@
|
|||||||
|
|
||||||
let migrateTarget: { sourceId: string; sourceName: string; iconUrl: string; manga: LibraryManga[] } | null = $state(null);
|
let migrateTarget: { sourceId: string; sourceName: string; iconUrl: string; manga: LibraryManga[] } | null = $state(null);
|
||||||
|
|
||||||
const allManga = $derived(groups.flatMap(g => g.manga));
|
// ── Derived filtered lists ────────────────────────────────────────────
|
||||||
|
const allManga = $derived(isLocal ? localItems : groups.flatMap(g => g.manga));
|
||||||
|
|
||||||
const filtered = $derived((() => {
|
const filtered = $derived((() => {
|
||||||
let items = allManga;
|
let items = allManga;
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
if (q) items = items.filter(m => m.title.toLowerCase().includes(q));
|
if (q && !isLocal) items = items.filter((m: any) => m.title.toLowerCase().includes(q));
|
||||||
if (activeFilters.unread) items = items.filter(m => m.unreadCount > 0);
|
if (!isLocal) {
|
||||||
if (activeFilters.downloaded) items = items.filter(m => m.downloadCount > 0);
|
if (activeFilters.unread) items = items.filter((m: any) => m.unreadCount > 0);
|
||||||
|
if (activeFilters.downloaded) items = items.filter((m: any) => m.downloadCount > 0);
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
})());
|
})());
|
||||||
|
|
||||||
let sourceNodes: SourceNode[] = $state([]);
|
|
||||||
|
|
||||||
$effect(() => { load(); });
|
$effect(() => { load(); });
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
|
if (isLocal) {
|
||||||
|
localPage = 1;
|
||||||
|
localItems = [];
|
||||||
|
const result = await getAdapter().browseSource('0', 1);
|
||||||
|
localItems = result.items;
|
||||||
|
localHasNext = result.hasNextPage;
|
||||||
|
localPage = 1;
|
||||||
|
} else {
|
||||||
const [libData, srcData] = await Promise.all([
|
const [libData, srcData] = await Promise.all([
|
||||||
getAdapter().getMangaList({}).then(r => ({ mangas: { nodes: r.items as any } })),
|
getAdapter().getMangaList({}).then(r => ({ mangas: { nodes: r.items as any } })),
|
||||||
getAdapter().getSources().then(nodes => ({ sources: { nodes } })),
|
getAdapter().getSources().then(nodes => ({ sources: { nodes } })),
|
||||||
]);
|
]);
|
||||||
sourceNodes = srcData.sources.nodes;
|
sourceNodes = srcData.sources.nodes;
|
||||||
groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName);
|
groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadMoreLocal() {
|
||||||
|
if (localLoadingMore || !localHasNext) return;
|
||||||
|
localLoadingMore = true;
|
||||||
|
try {
|
||||||
|
const next = localPage + 1;
|
||||||
|
const result = await getAdapter().browseSource('0', next);
|
||||||
|
localItems = [...localItems, ...result.items];
|
||||||
|
localHasNext = result.hasNextPage;
|
||||||
|
localPage = next;
|
||||||
|
} finally {
|
||||||
|
localLoadingMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchLocal() {
|
||||||
|
const q = searchInput.trim();
|
||||||
|
if (!q) { load(); return; }
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const result = await getAdapter().searchSource('0', q, 1);
|
||||||
|
localItems = result.items;
|
||||||
|
localHasNext = result.hasNextPage;
|
||||||
|
localPage = 1;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
search = q;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchKeydown(e: KeyboardEvent) {
|
||||||
|
if (!isLocal) return;
|
||||||
|
if (e.key === 'Enter') searchLocal();
|
||||||
|
if (e.key === 'Escape') { searchInput = ''; search = ''; load(); }
|
||||||
|
}
|
||||||
|
|
||||||
function toggleFilter(f: ContentFilter) {
|
function toggleFilter(f: ContentFilter) {
|
||||||
activeFilters = { ...activeFilters, [f]: !activeFilters[f] };
|
activeFilters = { ...activeFilters, [f]: !activeFilters[f] };
|
||||||
}
|
}
|
||||||
@@ -108,18 +166,31 @@
|
|||||||
<Thumbnail src={iconUrl} alt={extensionName} class="header-icon" onerror={(e) => ((e.target as HTMLImageElement).style.display = "none")} />
|
<Thumbnail src={iconUrl} alt={extensionName} class="header-icon" onerror={(e) => ((e.target as HTMLImageElement).style.display = "none")} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<span class="eyebrow">In Library</span>
|
<span class="eyebrow">{isLocal ? 'Local Source' : 'In Library'}</span>
|
||||||
<span class="title">{extensionName}</span>
|
<span class="title">{extensionName}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
<span class="count-badge">{filtered.length}{filtered.length !== allManga.length ? ` / ${allManga.length}` : ""}</span>
|
<span class="count-badge">
|
||||||
|
{isLocal ? allManga.length + (localHasNext ? '+' : '') : `${filtered.length}${filtered.length !== allManga.length ? ` / ${allManga.length}` : ''}`}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
<MagnifyingGlass size={12} class="search-icon" weight="light" />
|
<MagnifyingGlass size={12} class="search-icon" weight="light" />
|
||||||
|
{#if isLocal}
|
||||||
|
<input
|
||||||
|
class="search"
|
||||||
|
placeholder="Search…"
|
||||||
|
bind:value={searchInput}
|
||||||
|
autocomplete="off"
|
||||||
|
onkeydown={onSearchKeydown}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<input class="search" placeholder="Search" bind:value={search} autocomplete="off" />
|
<input class="search" placeholder="Search" bind:value={search} autocomplete="off" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !isLocal}
|
||||||
<div class="filter-wrap">
|
<div class="filter-wrap">
|
||||||
<button
|
<button
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
@@ -161,6 +232,7 @@
|
|||||||
<GearSix size={14} weight="bold" />
|
<GearSix size={14} weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,10 +248,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if filtered.length === 0}
|
{:else if filtered.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
{allManga.length === 0 ? "Nothing from this extension is in your library." : "No matches."}
|
{isLocal
|
||||||
|
? 'No manga found in local source. Add manga folders to your local source directory.'
|
||||||
|
: allManga.length === 0
|
||||||
|
? 'Nothing from this extension is in your library.'
|
||||||
|
: 'No matches.'}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if groups.length > 1}
|
{#if !isLocal && groups.length > 1}
|
||||||
<div class="source-groups">
|
<div class="source-groups">
|
||||||
{#each groups as group}
|
{#each groups as group}
|
||||||
<div class="source-group-header">
|
<div class="source-group-header">
|
||||||
@@ -192,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if groups.length === 1}
|
{:else if !isLocal && groups.length === 1}
|
||||||
<div class="single-source-bar">
|
<div class="single-source-bar">
|
||||||
<span class="source-group-name">{groups[0].displayName}</span>
|
<span class="source-group-name">{groups[0].displayName}</span>
|
||||||
<button class="migrate-btn" onclick={() => openMigrate(groups[0])} title="Migrate this source">
|
<button class="migrate-btn" onclick={() => openMigrate(groups[0])} title="Migrate this source">
|
||||||
@@ -214,6 +290,7 @@
|
|||||||
style="object-fit:{cropCovers ? 'cover' : 'contain'}"
|
style="object-fit:{cropCovers ? 'cover' : 'contain'}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
{#if !isLocal}
|
||||||
<div class="card-info-overlay" class:anim={anims} class:instant={!anims} class:always={statsAlways}>
|
<div class="card-info-overlay" class:anim={anims} class:instant={!anims} class:always={statsAlways}>
|
||||||
<div class="overlay-badges">
|
<div class="overlay-badges">
|
||||||
{#if isCompleted}
|
{#if isCompleted}
|
||||||
@@ -226,11 +303,25 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="card-title">{m.title}</p>
|
<p class="card-title">{m.title}</p>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if isLocal && localHasNext}
|
||||||
|
<div class="load-more">
|
||||||
|
<button class="load-more-btn" onclick={loadMoreLocal} disabled={localLoadingMore}>
|
||||||
|
{#if localLoadingMore}
|
||||||
|
<CircleNotch size={13} weight="light" class="anim-spin" />
|
||||||
|
Loading…
|
||||||
|
{:else}
|
||||||
|
Load more
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,7 +421,12 @@
|
|||||||
.cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); }
|
.cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); }
|
||||||
.title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
|
.title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
|
||||||
|
|
||||||
.empty { display: flex; align-items: center; justify-content: center; height: 60%; color: var(--text-muted); font-size: var(--text-sm); }
|
.empty { display: flex; align-items: center; justify-content: center; height: 60%; color: var(--text-muted); font-size: var(--text-sm); text-align: center; padding: 0 var(--sp-6); }
|
||||||
|
|
||||||
|
.load-more { display: flex; justify-content: center; padding: var(--sp-4) 0; }
|
||||||
|
.load-more-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 20px; border-radius: var(--radius-md); background: var(--bg-raised); color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||||
|
.load-more-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
|
||||||
|
.load-more-btn:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||||
</style>
|
</style>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let extensions: Extension[] = $state([]);
|
let extensions: Extension[] = $state([]);
|
||||||
let localMangaCount = $state(0);
|
let localMangaCount = $state<string>("0");
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let refreshing = $state(false);
|
let refreshing = $state(false);
|
||||||
let filter = $state<Filter>("installed");
|
let filter = $state<Filter>("installed");
|
||||||
@@ -84,8 +84,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadLocalManga() {
|
async function loadLocalManga() {
|
||||||
const d = await Promise.resolve(null);
|
try {
|
||||||
|
const r = await getAdapter().browseSource('0', 1)
|
||||||
|
localMangaCount = r.hasNextPage ? r.items.length + '+' : String(r.items.length)
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFromRepo() {
|
async function fetchFromRepo() {
|
||||||
@@ -338,7 +340,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="list">
|
<div class="list">
|
||||||
{#if showLocal}
|
{#if showLocal}
|
||||||
<div class="local-row">
|
<div class="local-row" style="cursor:pointer" onclick={() => libraryTarget = { pkgName: '__local__', extensionName: 'Local Source', iconUrl: '' }}>
|
||||||
<div class="local-icon"><HardDrives size={18} weight="bold" /></div>
|
<div class="local-icon"><HardDrives size={18} weight="bold" /></div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="name">Local Source</span>
|
<span class="name">Local Source</span>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||||
import { platformService } from '$lib/platform-service'
|
import { platformService } from '$lib/platform-service'
|
||||||
|
|
||||||
|
const isTauri = platformService.platform === 'tauri'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectOpen: string | null
|
selectOpen: string | null
|
||||||
closingSelect: string | null
|
closingSelect: string | null
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if isTauri}
|
||||||
<label class="s-row">
|
<label class="s-row">
|
||||||
<div class="s-row-info"><span class="s-label">Auto-start server</span><span class="s-desc">Launch tachidesk-server when Moku opens</span></div>
|
<div class="s-row-info"><span class="s-label">Auto-start server</span><span class="s-desc">Launch tachidesk-server when Moku opens</span></div>
|
||||||
<button role="switch" aria-checked={settingsState.settings.autoStartServer} aria-label="Auto-start server"
|
<button role="switch" aria-checked={settingsState.settings.autoStartServer} aria-label="Auto-start server"
|
||||||
@@ -84,6 +87,7 @@
|
|||||||
<span class="s-toggle-thumb"></span>
|
<span class="s-toggle-thumb"></span>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if serverAdvancedOpen}
|
{#if serverAdvancedOpen}
|
||||||
<div class="srv-adv-panel">
|
<div class="srv-adv-panel">
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
|||||||
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
|
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
|
||||||
const data = await this.gql<{
|
const data = await this.gql<{
|
||||||
fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean }
|
fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean }
|
||||||
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'LATEST', page })
|
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: sourceId === '0' ? 'POPULAR' : 'LATEST', page })
|
||||||
return {
|
return {
|
||||||
items: data.fetchSourceManga.mangas.map(mapManga),
|
items: data.fetchSourceManga.mangas.map(mapManga),
|
||||||
hasNextPage: data.fetchSourceManga.hasNextPage,
|
hasNextPage: data.fetchSourceManga.hasNextPage,
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ export function startProbe(
|
|||||||
boot.skipped = false
|
boot.skipped = false
|
||||||
boot.serverProbeOk = false
|
boot.serverProbeOk = false
|
||||||
appState.status = 'booting'
|
appState.status = 'booting'
|
||||||
|
|
||||||
|
if (appState.platform === 'web') {
|
||||||
|
boot.failed = true
|
||||||
|
appState.status = 'error'
|
||||||
|
startBackgroundProbe(gen, authMode, user, pass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let tries = 0
|
let tries = 0
|
||||||
|
|
||||||
async function probe() {
|
async function probe() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { appState, app } from '$lib/state/app.svelte'
|
import { appState, app } from '$lib/state/app.svelte'
|
||||||
|
import { boot } from '$lib/state/boot.svelte'
|
||||||
import { notifications } from '$lib/state/notifications.svelte'
|
import { notifications } from '$lib/state/notifications.svelte'
|
||||||
import { settingsState, loadSettingsIntoState, updateSettings } from '$lib/state/settings.svelte'
|
import { settingsState, loadSettingsIntoState, updateSettings } from '$lib/state/settings.svelte'
|
||||||
import { applyTheme, mountSystemThemeSync } from '$lib/core/theme'
|
import { applyTheme, mountSystemThemeSync } from '$lib/core/theme'
|
||||||
@@ -147,6 +148,7 @@
|
|||||||
mode={appState.status === 'locked' ? 'locked' : 'loading'}
|
mode={appState.status === 'locked' ? 'locked' : 'loading'}
|
||||||
{ringFull}
|
{ringFull}
|
||||||
failed={appState.status === 'error'}
|
failed={appState.status === 'error'}
|
||||||
|
notConfigured={boot.notConfigured}
|
||||||
pinLen={settingsState.settings.appLockPin?.length ?? 0}
|
pinLen={settingsState.settings.appLockPin?.length ?? 0}
|
||||||
pinCorrect={settingsState.settings.appLockPin ?? ''}
|
pinCorrect={settingsState.settings.appLockPin ?? ''}
|
||||||
onReady={onSplashReady}
|
onReady={onSplashReady}
|
||||||
|
|||||||
Reference in New Issue
Block a user