mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Chore: ModalBlur Component
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { readerState } from "$lib/state/reader.svelte";
|
||||
import { fetchPages } from "./pageLoader";
|
||||
import { cancelQueuedFetches, revokeBlobUrl } from "$lib/core/cache/imageCache";
|
||||
import { clearResolvedUrlCache } from "$lib/core/cache/pageCache";
|
||||
import { clearResolvedUrlCache, clearPageCache } from "$lib/core/cache/pageCache";
|
||||
|
||||
export function scheduleResumeDismiss() {
|
||||
setTimeout(() => { readerState.resumeFading = true; }, 1500);
|
||||
setTimeout(() => { readerState.resumeVisible = false; readerState.resumeFading = false; }, 2500);
|
||||
}
|
||||
|
||||
let prefetchedChapterId: number | null = null;
|
||||
|
||||
export async function loadChapter(
|
||||
id: number,
|
||||
useBlob: boolean,
|
||||
@@ -23,11 +25,16 @@ export async function loadChapter(
|
||||
cancelQueuedFetches();
|
||||
if (useBlob) {
|
||||
clearResolvedUrlCache();
|
||||
// revoke blob URLs for all loaded pages so the GPU can release their textures
|
||||
for (const url of readerState.pageUrls) revokeBlobUrl(url);
|
||||
for (const strip of readerState.stripChapters) {
|
||||
for (const url of strip.urls) revokeBlobUrl(url);
|
||||
}
|
||||
if (prefetchedChapterId !== null && prefetchedChapterId !== id) {
|
||||
const prefetchedUrls = await fetchPages(prefetchedChapterId, false).catch(() => [] as string[]);
|
||||
for (const url of prefetchedUrls) revokeBlobUrl(url);
|
||||
clearPageCache(prefetchedChapterId);
|
||||
}
|
||||
prefetchedChapterId = null;
|
||||
}
|
||||
|
||||
startAtLastPage.current = false;
|
||||
@@ -51,10 +58,13 @@ export async function loadChapter(
|
||||
else if (resumeTo > 1) readerState.pageNumber = Math.min(resumeTo, urls.length || resumeTo);
|
||||
readerState.pageReady = true;
|
||||
readerState.loading = false;
|
||||
if (adjacent.next) fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {});
|
||||
if (adjacent.next) {
|
||||
prefetchedChapterId = adjacent.next.id;
|
||||
fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (ctrl.signal.aborted) return;
|
||||
readerState.error = e instanceof Error ? e.message : String(e);
|
||||
readerState.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,7 @@
|
||||
/* ── Backdrop & Modal Shell ───────────────────────────────────────── */
|
||||
.s-backdrop {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
|
||||
z-index: var(--z-settings);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
animation: s-fade-in 0.14s ease both;
|
||||
@@ -29,10 +27,7 @@
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
animation: s-scale-in 0.2s cubic-bezier(0.16,1,0.3,1) both;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.04) inset,
|
||||
0 24px 80px rgba(0,0,0,0.7),
|
||||
0 8px 24px rgba(0,0,0,0.4);
|
||||
box-shadow: 0 0 0 1px var(--border-dim), 0 24px 64px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +41,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
border-radius: var(--radius-2xl) 0 0 var(--radius-2xl);
|
||||
}
|
||||
|
||||
@@ -140,7 +135,6 @@
|
||||
.s-content-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import ContentSettings from './sections/ContentSettings.svelte'
|
||||
import AboutSettings from './sections/AboutSettings.svelte'
|
||||
import DevtoolsSettings from './sections/DevToolsSettings.svelte'
|
||||
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||
|
||||
interface Props { onclose?: () => void; onOpenThemeEditor?: (id?: string | null) => void }
|
||||
let { onclose, onOpenThemeEditor }: Props = $props()
|
||||
@@ -111,6 +112,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalBlur />
|
||||
<div class="s-backdrop" role="presentation" tabindex="-1"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) close() }}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); close() } }}>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} from "$lib/state/series.svelte";
|
||||
import { app } from "$lib/state/app.svelte";
|
||||
import type { Manga, Chapter, Category } from "$lib/types";
|
||||
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||
|
||||
|
||||
let manga: Manga | null = $state(null);
|
||||
@@ -353,6 +354,7 @@
|
||||
</script>
|
||||
|
||||
{#if seriesState.previewManga}
|
||||
<ModalBlur blur={4} dim={0.72} />
|
||||
<div
|
||||
class="backdrop"
|
||||
role="button"
|
||||
@@ -679,10 +681,8 @@
|
||||
<style>
|
||||
.backdrop {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.72);
|
||||
z-index: var(--z-settings);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.12s ease both;
|
||||
}
|
||||
.modal {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
blur = 8,
|
||||
dim = 0.6,
|
||||
zIndex = 'var(--z-settings)',
|
||||
animate = true,
|
||||
}: {
|
||||
blur?: number
|
||||
dim?: number
|
||||
zIndex?: string | number
|
||||
animate?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="modal-blur"
|
||||
class:animate
|
||||
style="--blur:{blur}px; --dim:{dim}; --z:{zIndex}"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.modal-blur {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(var(--blur));
|
||||
-webkit-backdrop-filter: blur(var(--blur));
|
||||
background: rgba(0, 0, 0, var(--dim));
|
||||
pointer-events: none;
|
||||
z-index: var(--z);
|
||||
}
|
||||
|
||||
.modal-blur.animate {
|
||||
animation: blur-in 0.14s ease both;
|
||||
}
|
||||
|
||||
@keyframes blur-in {
|
||||
from { opacity: 0 }
|
||||
to { opacity: 1 }
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@
|
||||
import { markManyRead } from "$lib/request-manager/chapters";
|
||||
import type { Tracker, TrackRecord, TrackSearch } from "$lib/types";
|
||||
import type { Chapter } from "$lib/types";
|
||||
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||
|
||||
let { mangaId, mangaTitle, onClose }: {
|
||||
mangaId: number;
|
||||
@@ -250,6 +251,7 @@
|
||||
}
|
||||
}} />
|
||||
|
||||
<ModalBlur blur={4} dim={0.68} />
|
||||
<div class="backdrop" role="presentation" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
<div class="modal" role="dialog" aria-label="Tracking">
|
||||
|
||||
@@ -497,6 +499,7 @@
|
||||
{#if confirmUnbindId !== null}
|
||||
{@const rec = records.find(r => r.id === confirmUnbindId)}
|
||||
{@const trk = rec ? trackerFor(rec.trackerId) : null}
|
||||
<ModalBlur blur={2} dim={0.45} zIndex="calc(var(--z-settings) + 1)" />
|
||||
<div class="confirm-backdrop" role="button" tabindex="-1" aria-label="Cancel"
|
||||
onclick={() => confirmUnbindId = null}
|
||||
onkeydown={(e) => { if (e.key === "Escape") confirmUnbindId = null; }}>
|
||||
@@ -515,10 +518,9 @@
|
||||
|
||||
<style>
|
||||
.backdrop {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.68);
|
||||
position: fixed; inset: 0;
|
||||
z-index: var(--z-settings);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.12s ease both;
|
||||
}
|
||||
.modal {
|
||||
@@ -646,7 +648,7 @@
|
||||
.result-row:hover:not(:disabled) .result-action { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||
.result-action-on { color: var(--accent-fg) !important; border-color: var(--accent-dim) !important; background: var(--accent-muted) !important; }
|
||||
|
||||
.confirm-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-settings) + 1); background: rgba(0,0,0,0.45); backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; }
|
||||
.confirm-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-settings) + 1); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; }
|
||||
.confirm-modal { background: var(--bg-surface); border: 1px solid var(--border-dim); border-radius: var(--radius-xl); padding: var(--sp-5); width: 260px; display: flex; flex-direction: column; gap: var(--sp-3); box-shadow: 0 16px 48px rgba(0,0,0,0.5); animation: scaleIn 0.15s ease both; }
|
||||
.confirm-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary); margin: 0; }
|
||||
.confirm-body { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); line-height: 1.5; margin: 0; letter-spacing: var(--tracking-wide); }
|
||||
|
||||
Vendored
+11
-9
@@ -5,9 +5,9 @@ import { getUIAccessToken } from "$lib/core/auth";
|
||||
const cache = new Map<string, string>();
|
||||
const inflight = new Map<string, Promise<string>>();
|
||||
const MAX_CONCURRENT = 6;
|
||||
let active = 0;
|
||||
let active = 0;
|
||||
let drainScheduled = false;
|
||||
let clearing = false;
|
||||
let generation = 0;
|
||||
|
||||
interface QueueEntry {
|
||||
url: string;
|
||||
@@ -32,10 +32,11 @@ async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async function doFetch(url: string): Promise<string> {
|
||||
async function doFetch(url: string, gen: number): Promise<string> {
|
||||
const headers = await getAuthHeaders();
|
||||
const blob = await platformService.fetchImage(url, headers);
|
||||
if (clearing) throw new DOMException("Cancelled", "AbortError");
|
||||
if (gen !== generation) throw new DOMException("Cancelled", "AbortError");
|
||||
const blob = await platformService.fetchImage(url, headers);
|
||||
if (gen !== generation) throw new DOMException("Cancelled", "AbortError");
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
cache.set(url, blobUrl);
|
||||
return blobUrl;
|
||||
@@ -55,8 +56,9 @@ function drain() {
|
||||
drainScheduled = false;
|
||||
while (active < MAX_CONCURRENT && queue.length > 0) {
|
||||
const entry = queue.shift()!;
|
||||
const gen = generation;
|
||||
active++;
|
||||
doFetch(entry.url)
|
||||
doFetch(entry.url, gen)
|
||||
.then(entry.resolve, entry.reject)
|
||||
.finally(() => { active--; drain(); });
|
||||
}
|
||||
@@ -107,6 +109,7 @@ export function preloadBlobUrls(urls: string[], basePriority = 0): void {
|
||||
export function revokeBlobUrl(url: string): void {
|
||||
const blob = cache.get(url);
|
||||
if (blob) { URL.revokeObjectURL(blob); cache.delete(url); }
|
||||
inflight.delete(url);
|
||||
}
|
||||
|
||||
export function deprioritizeQueue(): void {
|
||||
@@ -123,10 +126,9 @@ export function cancelQueuedFetches(): void {
|
||||
}
|
||||
|
||||
export function clearBlobCache(): void {
|
||||
clearing = true;
|
||||
generation++;
|
||||
cancelQueuedFetches();
|
||||
inflight.clear();
|
||||
cache.forEach(blob => URL.revokeObjectURL(blob));
|
||||
cache.clear();
|
||||
inflight.clear();
|
||||
clearing = false;
|
||||
}
|
||||
Vendored
+5
-2
@@ -1,5 +1,5 @@
|
||||
import { getBlobUrl, preloadBlobUrls } from "$lib/core/cache/imageCache";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { getBlobUrl, preloadBlobUrls, revokeBlobUrl } from "$lib/core/cache/imageCache";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
|
||||
const pageCache = new Map<number, string[]>();
|
||||
const inflight = new Map<number, Promise<string[]>>();
|
||||
@@ -90,6 +90,9 @@ export function preloadImage(url: string, useBlob: boolean): void {
|
||||
}
|
||||
|
||||
export function clearResolvedUrlCache(): void {
|
||||
for (const promise of resolvedUrlCache.values()) {
|
||||
promise.then(blobUrl => { if (blobUrl) revokeBlobUrl(blobUrl); }).catch(() => {});
|
||||
}
|
||||
resolvedUrlCache.clear();
|
||||
aspectCache.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user