Chore: ModalBlur Component

This commit is contained in:
Youwes09
2026-06-09 21:08:57 -05:00
parent abd60f261f
commit 915ff66b2f
8 changed files with 82 additions and 29 deletions
+13 -3
View File
@@ -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,7 +58,10 @@ 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);
+3 -9
View File
@@ -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); }
+9 -7
View File
@@ -7,7 +7,7 @@ const inflight = new Map<string, Promise<string>>();
const MAX_CONCURRENT = 6;
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();
if (gen !== generation) throw new DOMException("Cancelled", "AbortError");
const blob = await platformService.fetchImage(url, headers);
if (clearing) throw new DOMException("Cancelled", "AbortError");
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;
}
+4 -1
View File
@@ -1,4 +1,4 @@
import { getBlobUrl, preloadBlobUrls } from "$lib/core/cache/imageCache";
import { getBlobUrl, preloadBlobUrls, revokeBlobUrl } from "$lib/core/cache/imageCache";
import { settingsState } from "$lib/state/settings.svelte";
const pageCache = new Map<number, 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();
}