mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 09:49:58 -05:00
Chore: Completed Splash-Screen & Iniital Tauri Wire-Up
This commit is contained in:
@@ -144,7 +144,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Reset virtual load window when chapter changes
|
||||
let lastChapterId = 0;
|
||||
$effect(() => {
|
||||
const chapterId = readerState.activeChapter?.id ?? 0;
|
||||
@@ -362,6 +361,8 @@
|
||||
readerState.inspectPanY = clampedY;
|
||||
}
|
||||
|
||||
let tapTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function handleTap(e: MouseEvent) {
|
||||
if (style === "longstrip") {
|
||||
if (stripDragMoved) { stripDragMoved = false; return; }
|
||||
@@ -369,7 +370,19 @@
|
||||
}
|
||||
if (inspectDragMoved) { inspectDragMoved = false; return; }
|
||||
if (stripDragMoved) { stripDragMoved = false; return; }
|
||||
onTap(e);
|
||||
if (tapToToggleBar) {
|
||||
if (tapTimer) { clearTimeout(tapTimer); tapTimer = null; return; }
|
||||
tapTimer = setTimeout(() => { tapTimer = null; onTap(e); }, 220);
|
||||
} else {
|
||||
onTap(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDblClick() {
|
||||
if (tapToToggleBar) {
|
||||
if (tapTimer) { clearTimeout(tapTimer); tapTimer = null; }
|
||||
onToggleUi();
|
||||
}
|
||||
}
|
||||
|
||||
function setContainer(el: HTMLDivElement) {
|
||||
@@ -399,7 +412,7 @@
|
||||
tabindex="-1"
|
||||
onclick={handleTap}
|
||||
onauxclick={(e) => { if (e.button === 1 && style === "longstrip") e.preventDefault(); }}
|
||||
ondblclick={() => { if (tapToToggleBar) onToggleUi(); }}
|
||||
ondblclick={handleDblClick}
|
||||
onmousedown={onInspectMouseDown}
|
||||
onpointerdown={pinchZoomEnabled ? onPointerDown : undefined}
|
||||
onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, untrack, tick } from "svelte";
|
||||
import { readerState, PAGE_STYLES } from "$lib/state/reader.svelte";
|
||||
import { settingsState, updateSettings } from "$lib/state/settings.svelte";
|
||||
import { app } from "$lib/state/app.svelte";
|
||||
import { DEFAULT_KEYBINDS } from "$lib/core/keybinds/defaultBinds";
|
||||
import { fetchPages, resolveUrl, preloadImage, measureAspect, buildPageGroups } from "$lib/components/reader/lib/pageLoader";
|
||||
import { setupScrollTracking, appendNextChapter } from "$lib/components/reader/lib/scrollHandler";
|
||||
@@ -10,6 +11,7 @@
|
||||
import { goForward, goBack, jumpToPage } from "$lib/components/reader/lib/navigation";
|
||||
import { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "$lib/components/reader/lib/zoomHelpers";
|
||||
import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader";
|
||||
import { historyState } from "$lib/state/history.svelte";
|
||||
import type { ReaderSettings } from "$lib/state/reader.svelte";
|
||||
import ReaderControls from "$lib/components/reader/ReaderControls.svelte";
|
||||
import PageView from "$lib/components/reader/PageView.svelte";
|
||||
@@ -54,7 +56,9 @@
|
||||
const isBookmarked = $derived(
|
||||
!!currentBookmark &&
|
||||
currentBookmark.chapterId === displayChapter?.id &&
|
||||
currentBookmark.pageNumber === readerState.pageNumber
|
||||
(style === "double"
|
||||
? currentGroup.includes(currentBookmark.pageNumber)
|
||||
: currentBookmark.pageNumber === readerState.pageNumber)
|
||||
);
|
||||
|
||||
const currentPageMarkers = $derived(displayChapter ? readerState.getMarkersForPage(displayChapter.id, readerState.pageNumber) : []);
|
||||
@@ -139,6 +143,7 @@
|
||||
let startAtLastPageRef = { current: false };
|
||||
let cleanupScroll: () => void = () => {};
|
||||
let stripChaptersRef = readerState.stripChapters;
|
||||
let tickTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
$effect(() => { stripChaptersRef = readerState.stripChapters; });
|
||||
|
||||
@@ -219,7 +224,7 @@
|
||||
resetZoom: () => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: 1.0 }); restoreZoomAnchor(containerEl, zoomAnchor); },
|
||||
cycleStyle: () => { const idx = PAGE_STYLES.indexOf(style); applySettings({ pageStyle: PAGE_STYLES[(idx + 1) % PAGE_STYLES.length] }); },
|
||||
toggleDirection: () => applySettings({ readingDirection: rtl ? "ltr" : "rtl" }),
|
||||
openSettings: () => { settingsState.settingsOpen = true; },
|
||||
openSettings: () => { app.setSettingsOpen(true); },
|
||||
toggleBookmark: () => toggleBookmark(displayChapter, readerState.pageNumber),
|
||||
toggleAutoScroll: () => { if (style === "longstrip") updateSettings({ autoScroll: !(settingsState.settings.autoScroll ?? false) }); },
|
||||
toggleMarker: () => {
|
||||
@@ -293,8 +298,39 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const ch = readerState.activeChapter;
|
||||
if (ch) untrack(() => loadChapter(ch.id, useBlob, abortCtrl, startAtLastPageRef, markedRead, adjacent));
|
||||
const ch = readerState.activeChapter;
|
||||
const manga = readerState.activeManga;
|
||||
if (ch && manga) {
|
||||
untrack(() => {
|
||||
historyState.openSession(
|
||||
manga.id, manga.title, manga.thumbnailUrl,
|
||||
ch.id, ch.name, readerState.pageNumber,
|
||||
);
|
||||
loadChapter(ch.id, useBlob, abortCtrl, startAtLastPageRef, markedRead, adjacent);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const page = readerState.pageNumber;
|
||||
const chId = style === "longstrip"
|
||||
? (readerState.visibleChapterId ?? readerState.activeChapter?.id)
|
||||
: readerState.activeChapter?.id;
|
||||
const chName = style === "longstrip"
|
||||
? (readerState.activeChapterList.find(c => c.id === chId)?.name ?? readerState.activeChapter?.name ?? "")
|
||||
: (readerState.activeChapter?.name ?? "");
|
||||
|
||||
if (!chId || !readerState.activeManga) return;
|
||||
|
||||
if (tickTimer) clearTimeout(tickTimer);
|
||||
tickTimer = setTimeout(() => {
|
||||
historyState.tickSession(chId, chName, page);
|
||||
tickTimer = null;
|
||||
}, 2_000);
|
||||
|
||||
return () => {
|
||||
if (tickTimer) { clearTimeout(tickTimer); tickTimer = null; }
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -518,6 +554,7 @@
|
||||
if (containerEl) ro.observe(containerEl);
|
||||
|
||||
return () => {
|
||||
historyState.closeSession();
|
||||
abortCtrl.current?.abort();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
if (roTimer) clearTimeout(roTimer);
|
||||
@@ -542,9 +579,14 @@
|
||||
role="presentation"
|
||||
onmousemove={(e) => {
|
||||
if (!tapToToggleBar) {
|
||||
if (barPosition === "top" && (e.clientY < 60 || window.innerHeight - e.clientY < 60)) showUi();
|
||||
if (barPosition === "left" && e.clientX < 60) showUi();
|
||||
if (barPosition === "right" && window.innerWidth - e.clientX < 60) showUi();
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
if (barPosition === "top" && (y < 60 || h - y < 60)) showUi();
|
||||
if (barPosition === "left" && x < 60) showUi();
|
||||
if (barPosition === "right" && w - x < 60) showUi();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -564,8 +606,7 @@
|
||||
onDeleteMarker={deleteCurrentMarker}
|
||||
onClampZoom={clampZoom}
|
||||
onApplySettings={applySettings}
|
||||
onDlOpen={() => readerState.dlOpen = true}
|
||||
onSettingsOpen={() => { settingsState.settingsOpen = true; }}
|
||||
onSettingsOpen={() => { app.setSettingsOpen(true); }}
|
||||
{perMangaEnabled}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
X, CaretLeft, CaretRight, CaretUp, CaretDown,
|
||||
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||
Bookmark, MapPin, Download, Check, GearSix, Sliders,
|
||||
ArrowsOut, ArrowsIn,
|
||||
} from "phosphor-svelte";
|
||||
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX } from "$lib/state/reader.svelte";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import { cubicOut, cubicIn } from "svelte/easing";
|
||||
import type { Chapter } from "$lib/types";
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
interface Props {
|
||||
displayChapter: Chapter | null;
|
||||
adjacent: { prev: Chapter | null; next: Chapter | null };
|
||||
adjacent: { prev: Chapter | null; next: Chapter | null; remaining: Chapter[] };
|
||||
visibleChunkLastPage: number;
|
||||
zoom: number;
|
||||
zoomPct: number;
|
||||
@@ -33,7 +34,6 @@
|
||||
onDeleteMarker: () => void;
|
||||
onClampZoom: (z: number) => number;
|
||||
onApplySettings: (patch: Partial<ReaderSettings>) => void;
|
||||
onDlOpen: () => void;
|
||||
onSettingsOpen: () => void;
|
||||
perMangaEnabled: boolean;
|
||||
}
|
||||
@@ -46,10 +46,37 @@
|
||||
barPosition, progressBar,
|
||||
onCaptureZoomAnchor, onRestoreZoomAnchor,
|
||||
onMaybeMarkRead, onToggleBookmark, onCommitMarker, onDeleteMarker,
|
||||
onClampZoom, onApplySettings, onDlOpen, onSettingsOpen,
|
||||
onClampZoom, onApplySettings, onSettingsOpen,
|
||||
perMangaEnabled,
|
||||
}: Props = $props();
|
||||
|
||||
const queueable = $derived(adjacent.remaining.filter(c => !c.downloaded));
|
||||
|
||||
async function gqlMutation(query: string, variables: Record<string, unknown>): Promise<void> {
|
||||
const base = settingsState.settings.serverUrl ?? "http://localhost:4567";
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
const mode = settingsState.settings.serverAuthMode ?? "NONE";
|
||||
if (mode === "BASIC_AUTH") {
|
||||
const u = settingsState.settings.serverAuthUser?.trim() ?? "";
|
||||
const p = settingsState.settings.serverAuthPass?.trim() ?? "";
|
||||
if (u && p) headers["Authorization"] = `Basic ${btoa(`${u}:${p}`)}`;
|
||||
}
|
||||
const res = await fetch(`${base}/api/graphql`, { method: "POST", headers, body: JSON.stringify({ query, variables }) });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
if (json.errors?.length) throw new Error(json.errors[0].message);
|
||||
}
|
||||
|
||||
const ENQUEUE_ONE = `mutation EnqueueOne($id: Int!) { fetchChapterPages(input: { chapterId: $id }) { chapter { id } } }`;
|
||||
const ENQUEUE_MANY = `mutation EnqueueMany($ids: [Int!]!) { enqueueChaptersDownloads(input: { ids: $ids }) { downloadStatus { queue { chapter { id } } } } }`;
|
||||
|
||||
async function runDl(fn: () => Promise<void>) {
|
||||
readerState.dlBusy = true;
|
||||
try { await fn(); } catch (e) { console.error(e); }
|
||||
readerState.dlBusy = false;
|
||||
readerState.dlOpen = false;
|
||||
}
|
||||
|
||||
const isVertical = $derived(barPosition === "left" || barPosition === "right");
|
||||
const popoverSide = $derived(
|
||||
barPosition === "left" ? "right" :
|
||||
@@ -74,20 +101,15 @@
|
||||
else await document.exitFullscreen();
|
||||
}
|
||||
|
||||
let wcTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function wcResetTimer() {
|
||||
if (wcTimer) clearTimeout(wcTimer);
|
||||
wcTimer = setTimeout(() => { readerState.winOpen = false; }, 1500);
|
||||
function closeAllPopovers() {
|
||||
readerState.actionsOpen = false;
|
||||
readerState.markerOpen = false;
|
||||
readerState.zoomOpen = false;
|
||||
readerState.dlOpen = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (readerState.winOpen) wcResetTimer();
|
||||
else if (wcTimer) { clearTimeout(wcTimer); wcTimer = null; }
|
||||
return () => { if (wcTimer) clearTimeout(wcTimer); };
|
||||
});
|
||||
|
||||
function openMarkerPopover() {
|
||||
closeAllPopovers();
|
||||
if (currentPageMarkers.length > 0) {
|
||||
const first = currentPageMarkers[0];
|
||||
readerState.openMarker(first.id, first.note, first.color);
|
||||
@@ -96,7 +118,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let chapterHover = $state(false);
|
||||
let chapterHover = $state(false);
|
||||
let chapterHoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function showChapterPopover() {
|
||||
@@ -117,18 +139,17 @@
|
||||
class:hidden={!uiVisible}
|
||||
>
|
||||
<div class="bar-start">
|
||||
<button class="icon-btn" onclick={() => readerState.closeReader()} title="Close reader">
|
||||
<X size={15} weight="light" />
|
||||
<button class="icon-btn close-btn" onclick={() => readerState.closeReader()} title="Close reader">
|
||||
<X size={14} weight="regular" />
|
||||
</button>
|
||||
|
||||
<div class="bar-divider"></div>
|
||||
|
||||
<button class="icon-btn"
|
||||
onclick={() => { if (adjacent.prev) { onMaybeMarkRead(); readerState.openReader(adjacent.prev, readerState.activeChapterList); } }}
|
||||
disabled={!adjacent.prev}>
|
||||
{#if isVertical}
|
||||
<CaretUp size={14} weight="light" />
|
||||
{:else}
|
||||
<CaretLeft size={14} weight="light" />
|
||||
{/if}
|
||||
disabled={!adjacent.prev}
|
||||
title="Previous chapter">
|
||||
{#if isVertical}<CaretUp size={13} weight="regular" />{:else}<CaretLeft size={13} weight="regular" />{/if}
|
||||
</button>
|
||||
|
||||
<div
|
||||
@@ -137,7 +158,7 @@
|
||||
onmouseleave={hideChapterPopover}
|
||||
role="presentation"
|
||||
>
|
||||
<button class="ch-pill" title="{readerState.activeManga?.title} / {displayChapter?.name}">
|
||||
<div class="ch-pill">
|
||||
{#if isVertical}
|
||||
<span class="ch-info"></span>
|
||||
{:else}
|
||||
@@ -148,33 +169,28 @@
|
||||
<span class="ch-name">{displayChapter?.name}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ch-page">p.{readerState.pageNumber} / {visibleChunkLastPage}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if !isVertical}
|
||||
<span class="ch-page">{readerState.pageNumber}<span class="ch-page-sep">/</span>{visibleChunkLastPage}</span>
|
||||
{/if}
|
||||
|
||||
{#if chapterHover && !isVertical}
|
||||
{#if chapterHover && isVertical}
|
||||
<div class="ch-popover ch-popover-{popoverSide}">
|
||||
<span class="ch-pop-title">{readerState.activeManga?.title}</span>
|
||||
<span class="ch-pop-sep">/</span>
|
||||
<span class="ch-pop-name">{displayChapter?.name}</span>
|
||||
<span class="ch-pop-page">p.{readerState.pageNumber} / {visibleChunkLastPage}</span>
|
||||
<span class="ch-pop-page">{readerState.pageNumber} / {visibleChunkLastPage}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="icon-btn"
|
||||
onclick={() => { if (adjacent.next) { onMaybeMarkRead(); readerState.openReader(adjacent.next, readerState.activeChapterList); } }}
|
||||
disabled={!adjacent.next}>
|
||||
{#if isVertical}
|
||||
<CaretDown size={14} weight="light" />
|
||||
{:else}
|
||||
<CaretRight size={14} weight="light" />
|
||||
{/if}
|
||||
disabled={!adjacent.next}
|
||||
title="Next chapter">
|
||||
{#if isVertical}<CaretDown size={13} weight="regular" />{:else}<CaretRight size={13} weight="regular" />{/if}
|
||||
</button>
|
||||
|
||||
{#if !isVertical}
|
||||
<span class="bar-sep" data-tauri-drag-region></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isVertical && progressBar}
|
||||
@@ -188,32 +204,36 @@
|
||||
{/if}
|
||||
|
||||
<div class="bar-end">
|
||||
<div class="zoom-wrap">
|
||||
<div class="zoom-inline">
|
||||
<button class="icon-btn zoom-icon-btn" onclick={() => adjustZoom(-ZOOM_STEP)} title="Zoom out" disabled={zoom <= ZOOM_MIN}>
|
||||
<MagnifyingGlassMinus size={13} weight="light" />
|
||||
</button>
|
||||
<div class="zoom-divider"></div>
|
||||
<button class="zoom-pct-btn" onclick={() => readerState.zoomOpen = !readerState.zoomOpen} title="Click to adjust zoom">
|
||||
{zoomPct}%
|
||||
</button>
|
||||
<div class="zoom-divider"></div>
|
||||
<button class="icon-btn zoom-icon-btn" onclick={() => adjustZoom(ZOOM_STEP)} title="Zoom in" disabled={zoom >= ZOOM_MAX}>
|
||||
<MagnifyingGlassPlus size={13} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="zoom-cluster">
|
||||
<button class="icon-btn zoom-step-btn" onclick={() => adjustZoom(-ZOOM_STEP)} title="Zoom out" disabled={zoom <= ZOOM_MIN}>
|
||||
<MagnifyingGlassMinus size={13} weight="regular" />
|
||||
</button>
|
||||
<button class="zoom-pct-btn" onclick={() => { readerState.zoomOpen = !readerState.zoomOpen; readerState.actionsOpen = false; readerState.markerOpen = false; }} title="Adjust zoom">
|
||||
{zoomPct}%
|
||||
</button>
|
||||
<button class="icon-btn zoom-step-btn" onclick={() => adjustZoom(ZOOM_STEP)} title="Zoom in" disabled={zoom >= ZOOM_MAX}>
|
||||
<MagnifyingGlassPlus size={13} weight="regular" />
|
||||
</button>
|
||||
|
||||
{#if readerState.zoomOpen}
|
||||
<div class="popover zoom-popover popover-{popoverSide}">
|
||||
<div class="zoom-slider-row">
|
||||
<input type="range" class="zoom-slider" min={10} max={100} step={5} value={zoomPct}
|
||||
<div class="popover zoom-popover popover-{popoverSide}" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="zoom-row">
|
||||
<button class="zoom-step-sm" onclick={() => adjustZoom(-ZOOM_STEP)} disabled={zoom <= ZOOM_MIN}>−</button>
|
||||
<input type="range" class="zoom-slider" min={10} max={200} step={5} value={zoomPct}
|
||||
oninput={(e) => { onCaptureZoomAnchor(); onApplySettings({ readerZoom: onClampZoom(Number(e.currentTarget.value) / 100) }); onRestoreZoomAnchor(); }} />
|
||||
<button class="zoom-step-sm" onclick={() => adjustZoom(ZOOM_STEP)} disabled={zoom >= ZOOM_MAX}>+</button>
|
||||
</div>
|
||||
<div class="zoom-footer">
|
||||
<span class="zoom-readout">{zoomPct}%</span>
|
||||
<button class="zoom-reset" onclick={resetZoom} disabled={zoom === 1.0}>Reset</button>
|
||||
</div>
|
||||
<button class="zoom-reset" onclick={resetZoom} disabled={zoom === 1.0}>Reset</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bar-divider"></div>
|
||||
|
||||
<div class="marker-wrap">
|
||||
<button
|
||||
class="icon-btn"
|
||||
@@ -227,44 +247,28 @@
|
||||
</button>
|
||||
|
||||
{#if readerState.markerOpen}
|
||||
<div class="popover marker-popover popover-{popoverSide}" role="presentation"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="popover marker-popover popover-{popoverSide}" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="marker-pop-header">
|
||||
<span class="marker-pop-title">
|
||||
{readerState.markerEditId ? "Edit marker" : "New marker"} · p.{readerState.pageNumber}
|
||||
</span>
|
||||
<span class="marker-pop-title">{readerState.markerEditId ? "Edit marker" : "New marker"} · p.{readerState.pageNumber}</span>
|
||||
{#if readerState.markerEditId}
|
||||
<button class="marker-delete-btn" onclick={onDeleteMarker} title="Delete marker">
|
||||
<X size={12} weight="light" />
|
||||
</button>
|
||||
<button class="marker-delete-btn" onclick={onDeleteMarker} title="Delete marker"><X size={12} weight="light" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="marker-color-row">
|
||||
{#each MARKER_COLORS as c}
|
||||
<button
|
||||
class="marker-swatch"
|
||||
class:marker-swatch-active={readerState.markerColor === c}
|
||||
style="--swatch:{MARKER_COLOR_HEX[c]}"
|
||||
onclick={() => readerState.markerColor = c}
|
||||
title={c}
|
||||
>
|
||||
<button class="marker-swatch" class:marker-swatch-active={readerState.markerColor === c}
|
||||
style="--swatch:{MARKER_COLOR_HEX[c]}" onclick={() => readerState.markerColor = c} title={c}>
|
||||
<span class="swatch-dot"></span>
|
||||
<span class="swatch-label">{c}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<textarea
|
||||
class="marker-textarea"
|
||||
style="--accent-marker:{MARKER_COLOR_HEX[readerState.markerColor]}"
|
||||
rows={3}
|
||||
placeholder="Note (optional)…"
|
||||
bind:value={readerState.markerNote}
|
||||
<textarea class="marker-textarea" style="--accent-marker:{MARKER_COLOR_HEX[readerState.markerColor]}"
|
||||
rows={3} placeholder="Note (optional)…" bind:value={readerState.markerNote}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onCommitMarker(); }
|
||||
if (e.key === "Escape") readerState.markerOpen = false;
|
||||
}}
|
||||
></textarea>
|
||||
}}></textarea>
|
||||
<div class="marker-pop-actions">
|
||||
<button class="marker-save-btn" style="--accent-marker:{MARKER_COLOR_HEX[readerState.markerColor]}" onclick={onCommitMarker}>
|
||||
<Check size={12} weight="bold" />
|
||||
@@ -278,67 +282,90 @@
|
||||
|
||||
<button class="icon-btn" class:active={isBookmarked} onclick={onToggleBookmark}
|
||||
title={isBookmarked ? "Remove bookmark" : "Bookmark this page"}>
|
||||
<Bookmark size={15} weight={isBookmarked ? "fill" : "regular"} />
|
||||
<Bookmark size={14} weight={isBookmarked ? "fill" : "regular"} />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" onclick={onDlOpen}>
|
||||
<Download size={14} weight="light" />
|
||||
</button>
|
||||
<div class="bar-divider"></div>
|
||||
|
||||
<button class="icon-btn" class:active={perMangaEnabled}
|
||||
onclick={() => { readerState.presetOpen = true; readerState.markerOpen = false; readerState.zoomOpen = false; readerState.dlOpen = false; }}
|
||||
onclick={() => { readerState.presetOpen = true; closeAllPopovers(); }}
|
||||
title="Reader settings">
|
||||
<Sliders size={13} weight="regular" />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" onclick={onSettingsOpen} title="Settings">
|
||||
<GearSix size={13} weight="regular" />
|
||||
</button>
|
||||
|
||||
<div class="wc-wrap">
|
||||
<div class="actions-wrap">
|
||||
<button
|
||||
class="icon-btn"
|
||||
class:active={readerState.winOpen}
|
||||
onclick={() => { readerState.winOpen = !readerState.winOpen; readerState.markerOpen = false; readerState.zoomOpen = false; readerState.dlOpen = false; }}
|
||||
title="Window controls"
|
||||
class:active={readerState.actionsOpen}
|
||||
onclick={() => { readerState.actionsOpen = !readerState.actionsOpen; readerState.markerOpen = false; readerState.zoomOpen = false; }}
|
||||
title="More actions"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<circle cx="6" cy="1.5" r="1.2" fill="currentColor"/>
|
||||
<circle cx="6" cy="6" r="1.2" fill="currentColor"/>
|
||||
<circle cx="6" cy="10.5" r="1.2" fill="currentColor"/>
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
||||
<circle cx="2" cy="6.5" r="1.3" fill="currentColor"/>
|
||||
<circle cx="6.5" cy="6.5" r="1.3" fill="currentColor"/>
|
||||
<circle cx="11" cy="6.5" r="1.3" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
{#if readerState.winOpen}
|
||||
|
||||
{#if readerState.actionsOpen}
|
||||
<div
|
||||
class="wc-clip wc-clip-{popoverSide}"
|
||||
class="popover actions-popover popover-{popoverSide}"
|
||||
role="presentation"
|
||||
onmouseenter={wcResetTimer}
|
||||
onmousemove={wcResetTimer}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
in:fly={isVertical
|
||||
? (barPosition === "left" ? { x: -8, duration: 160, easing: cubicOut } : { x: 8, duration: 160, easing: cubicOut })
|
||||
: { y: -6, duration: 160, easing: cubicOut }}
|
||||
out:fly={isVertical
|
||||
? (barPosition === "left" ? { x: -8, duration: 120, easing: cubicIn } : { x: 8, duration: 120, easing: cubicIn })
|
||||
: { y: -6, duration: 120, easing: cubicIn }}
|
||||
>
|
||||
<div
|
||||
class="wc-bar"
|
||||
role="presentation"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
in:fly={isVertical
|
||||
? (barPosition === "left" ? { x: '-100%', duration: 200, easing: cubicOut } : { x: '100%', duration: 200, easing: cubicOut })
|
||||
: { y: '-100%', duration: 200, easing: cubicOut }}
|
||||
out:fly={isVertical
|
||||
? (barPosition === "left" ? { x: '-100%', duration: 150, easing: cubicIn } : { x: '100%', duration: 150, easing: cubicIn })
|
||||
: { y: '-100%', duration: 150, easing: cubicIn }}
|
||||
>
|
||||
<button class="wc-icon-btn" onclick={async () => { readerState.winOpen = false; await toggleFullscreen(); }} title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}>
|
||||
{#if isFullscreen}
|
||||
<svg width="11" height="11" viewBox="0 0 11 11">
|
||||
<polyline points="1,4 1,1 4,1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7,1 10,1 10,4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="10,7 10,10 7,10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="4,10 1,10 1,7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.75" y="0.75" width="8.5" height="8.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
{/if}
|
||||
<button class="action-row" onclick={() => { readerState.dlOpen = !readerState.dlOpen; readerState.actionsOpen = false; }}>
|
||||
<Download size={13} weight="regular" />
|
||||
<span>Download</span>
|
||||
</button>
|
||||
<button class="action-row" onclick={() => { onSettingsOpen(); readerState.actionsOpen = false; }}>
|
||||
<GearSix size={13} weight="regular" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<div class="action-divider"></div>
|
||||
<button class="action-row" onclick={async () => { readerState.actionsOpen = false; await toggleFullscreen(); }}>
|
||||
{#if isFullscreen}
|
||||
<ArrowsIn size={13} weight="regular" />
|
||||
<span>Exit fullscreen</span>
|
||||
{:else}
|
||||
<ArrowsOut size={13} weight="regular" />
|
||||
<span>Fullscreen</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if readerState.dlOpen && readerState.activeChapter}
|
||||
{@const chapter = readerState.activeChapter}
|
||||
<div class="popover dl-popover popover-{popoverSide}" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<p class="dl-title">Download</p>
|
||||
<button class="dl-option" disabled={readerState.dlBusy || !!chapter.downloaded}
|
||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_ONE, { id: chapter.id }))}>
|
||||
This chapter
|
||||
<span class="dl-sub">{chapter.downloaded ? "Already downloaded" : chapter.name}</span>
|
||||
</button>
|
||||
<div class="dl-row">
|
||||
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_MANY, { ids: queueable.slice(0, readerState.nextN).map(c => c.id) }))}>
|
||||
Next chapters
|
||||
<span class="dl-sub">{Math.min(readerState.nextN, queueable.length)} not yet downloaded</span>
|
||||
</button>
|
||||
<div class="dl-stepper" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<button class="dl-step-btn" onclick={() => readerState.nextN = Math.max(1, readerState.nextN - 1)} disabled={readerState.nextN <= 1}>−</button>
|
||||
<span class="dl-step-val">{readerState.nextN}</span>
|
||||
<button class="dl-step-btn" onclick={() => readerState.nextN = Math.min(queueable.length || 1, readerState.nextN + 1)} disabled={readerState.nextN >= queueable.length}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_MANY, { ids: queueable.map(c => c.id) }))}>
|
||||
All remaining
|
||||
<span class="dl-sub">{queueable.length} not yet downloaded</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -349,12 +376,14 @@
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
background: var(--bg-void);
|
||||
gap: 2px;
|
||||
background: color-mix(in srgb, var(--bg-void) 92%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: opacity 0.25s ease;
|
||||
transition: opacity 0.2s ease;
|
||||
overflow: visible;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -362,9 +391,8 @@
|
||||
|
||||
.bar-top {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--sp-3);
|
||||
height: 40px;
|
||||
padding: 0 var(--sp-2);
|
||||
height: 44px;
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
@@ -372,12 +400,13 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-3) 0;
|
||||
width: 40px;
|
||||
width: 44px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
border-bottom: none;
|
||||
gap: 0;
|
||||
}
|
||||
.bar-left { left: 0; border-right: 1px solid var(--border-dim); }
|
||||
.bar-right { right: 0; border-left: 1px solid var(--border-dim); }
|
||||
@@ -385,20 +414,58 @@
|
||||
.bar-drag-gap { flex: 1; height: 100%; cursor: grab; }
|
||||
.bar-drag-gap:active { cursor: grabbing; }
|
||||
|
||||
.bar-start, .bar-end { display: flex; align-items: center; gap: var(--sp-1); }
|
||||
.bar-top .bar-start { overflow: hidden; }
|
||||
.bar-left .bar-start,
|
||||
.bar-left .bar-end,
|
||||
.bar-right .bar-start,
|
||||
.bar-right .bar-end { flex-direction: column; }
|
||||
.bar-start, .bar-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bar-top .bar-start { overflow: hidden; min-width: 0; }
|
||||
.bar-left .bar-start, .bar-left .bar-end,
|
||||
.bar-right .bar-start, .bar-right .bar-end { flex-direction: column; }
|
||||
|
||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); color: var(--text-muted); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); }
|
||||
.bar-middle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--sp-1) 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bar-divider {
|
||||
flex-shrink: 0;
|
||||
background: var(--border-dim);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.bar-top .bar-divider { width: 1px; height: 18px; margin: 0 var(--sp-1); }
|
||||
.bar-left .bar-divider,
|
||||
.bar-right .bar-divider { height: 1px; width: 20px; margin: var(--sp-1) 0; }
|
||||
|
||||
.icon-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 30px; height: 30px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.icon-btn:disabled { opacity: 0.2; cursor: default; }
|
||||
.icon-btn.active { color: var(--accent-fg); }
|
||||
.marker-btn-has { color: var(--marker-color, var(--accent-fg)) !important; }
|
||||
|
||||
.ch-hover-wrap { position: relative; min-width: 0; display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.close-btn:hover { color: var(--text-primary); background: color-mix(in srgb, #c0392b 15%, transparent); }
|
||||
|
||||
.ch-hover-wrap {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.ch-pill {
|
||||
display: flex;
|
||||
@@ -408,23 +475,39 @@
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
cursor: default;
|
||||
transition: background var(--t-fast);
|
||||
padding: 3px 6px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid transparent;
|
||||
transition: border-color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.ch-hover-wrap:hover .ch-pill {
|
||||
border-color: var(--border-dim);
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
.bar-left .ch-pill, .bar-right .ch-pill {
|
||||
width: 30px; height: 30px; justify-content: center; padding: 0; border: none;
|
||||
}
|
||||
.bar-left .ch-pill, .bar-right .ch-pill { width: 28px; height: 28px; justify-content: center; padding: 0; }
|
||||
.ch-info { font-size: 15px; line-height: 1; color: var(--text-faint); flex-shrink: 0; }
|
||||
|
||||
.ch-marquee-track { overflow-x: auto; min-width: 0; flex: 1; scrollbar-width: none; }
|
||||
.ch-marquee-track::-webkit-scrollbar { display: none; }
|
||||
.ch-marquee-content { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
|
||||
|
||||
.ch-title { color: var(--text-secondary); font-weight: var(--weight-medium); }
|
||||
.ch-sep { color: var(--text-faint); flex-shrink: 0; }
|
||||
.ch-name { color: var(--text-muted); }
|
||||
.ch-page { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
|
||||
|
||||
.ch-page {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
.ch-page-sep { color: var(--border-strong); }
|
||||
|
||||
.ch-popover {
|
||||
position: absolute;
|
||||
@@ -432,9 +515,7 @@
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
display: flex; align-items: center; gap: var(--sp-2);
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
@@ -447,34 +528,52 @@
|
||||
.ch-pop-title { color: var(--text-secondary); font-weight: var(--weight-medium); }
|
||||
.ch-pop-sep { color: var(--text-faint); }
|
||||
.ch-pop-name { color: var(--text-muted); }
|
||||
.ch-pop-page { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
.ch-pop-page { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.bar-sep { width: 1px; height: 16px; background: var(--border-dim); flex-shrink: 0; margin: 0 var(--sp-1); }
|
||||
.zoom-cluster {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-overlay);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bar-left .zoom-cluster, .bar-right .zoom-cluster { flex-direction: column; }
|
||||
|
||||
.zoom-wrap { position: relative; flex-shrink: 0; }
|
||||
.zoom-inline { display: flex; align-items: center; }
|
||||
.bar-left .zoom-inline, .bar-right .zoom-inline { flex-direction: column; }
|
||||
|
||||
.zoom-icon-btn { width: 28px; height: 28px; }
|
||||
.zoom-divider { background: var(--border-dim); flex-shrink: 0; }
|
||||
.bar-top .zoom-divider { width: 1px; height: 16px; }
|
||||
.bar-left .zoom-divider,
|
||||
.bar-right .zoom-divider { height: 1px; width: 16px; }
|
||||
.zoom-step-btn {
|
||||
width: 26px; height: 26px;
|
||||
border-radius: calc(var(--radius-md) - 1px);
|
||||
color: var(--text-faint);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.zoom-step-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-step-btn:disabled { opacity: 0.2; cursor: default; }
|
||||
|
||||
.zoom-pct-btn {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--text-secondary);
|
||||
height: 28px;
|
||||
min-width: 38px;
|
||||
height: 26px;
|
||||
min-width: 36px;
|
||||
padding: 0 2px;
|
||||
text-align: center;
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
padding: 0 var(--sp-1);
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
border-radius: 0;
|
||||
border-left: 1px solid var(--border-dim);
|
||||
border-right: 1px solid var(--border-dim);
|
||||
}
|
||||
.bar-left .zoom-pct-btn, .bar-right .zoom-pct-btn {
|
||||
height: 22px; min-width: unset; width: 26px;
|
||||
writing-mode: vertical-rl; font-size: 9px;
|
||||
rotate: 270deg;
|
||||
border-left: none; border-right: none;
|
||||
border-top: 1px solid var(--border-dim);
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
.bar-left .zoom-pct-btn,
|
||||
.bar-right .zoom-pct-btn { height: 24px; min-width: unset; width: 28px; writing-mode: vertical-rl; font-size: 9px; padding: var(--sp-1) 0; }
|
||||
.zoom-pct-btn:hover { color: var(--text-primary); background: var(--bg-raised); }
|
||||
|
||||
.popover {
|
||||
@@ -482,19 +581,36 @@
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.55);
|
||||
z-index: 100;
|
||||
animation: scaleIn 0.1s ease both;
|
||||
animation: scaleIn 0.12s ease both;
|
||||
}
|
||||
.popover-bottom { top: calc(100% + 6px); left: 50%; translate: -50% 0; transform-origin: top center; }
|
||||
.popover-bottom { top: calc(100% + 8px); left: 50%; translate: -50% 0; transform-origin: top center; }
|
||||
.popover-right { left: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: left center; }
|
||||
.popover-left { right: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: right center; }
|
||||
.actions-wrap .popover-bottom { left: auto; right: 0; translate: none; transform-origin: top right; }
|
||||
.actions-wrap .popover-right { top: auto; bottom: 0; translate: none; transform-origin: bottom left; }
|
||||
.actions-wrap .popover-left { top: auto; bottom: 0; translate: none; transform-origin: bottom right; }
|
||||
|
||||
.zoom-popover { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); min-width: 180px; }
|
||||
.zoom-slider-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.zoom-slider { flex: 1; height: 3px; appearance: none; -webkit-appearance: none; background: var(--border-strong); border-radius: 2px; outline: none; cursor: pointer; }
|
||||
.zoom-popover { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); min-width: 200px; }
|
||||
.zoom-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.zoom-step-sm {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 24px; height: 24px; flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-base); line-height: 1;
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.zoom-step-sm:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-step-sm:disabled { opacity: 0.25; cursor: default; }
|
||||
.zoom-slider { flex: 1; height: 3px; appearance: none; -webkit-appearance: none; background: var(--border-strong); border-radius: 2px; outline: none; cursor: pointer; }
|
||||
.zoom-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--accent-fg); cursor: pointer; }
|
||||
.zoom-reset { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); padding: 3px var(--sp-2); border-radius: var(--radius-sm); border: 1px solid var(--border-dim); transition: color var(--t-base), background var(--t-base), border-color var(--t-base); }
|
||||
.zoom-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||
.zoom-readout { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); font-variant-numeric: tabular-nums; }
|
||||
.zoom-reset { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); padding: 3px var(--sp-2); border-radius: var(--radius-sm); border: 1px solid var(--border-dim); transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast); }
|
||||
.zoom-reset:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-overlay); border-color: var(--border-strong); }
|
||||
.zoom-reset:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
@@ -507,7 +623,7 @@
|
||||
.marker-color-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.marker-swatch { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 4px; border-radius: var(--radius-sm); background: none; border: none; cursor: pointer; flex: 1; transition: background var(--t-fast); }
|
||||
.marker-swatch:hover { background: var(--bg-overlay); }
|
||||
.swatch-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--swatch); box-shadow: 0 0 0 0 var(--swatch); transition: box-shadow var(--t-fast), transform var(--t-fast); flex-shrink: 0; }
|
||||
.swatch-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--swatch); transition: box-shadow var(--t-fast), transform var(--t-fast); flex-shrink: 0; }
|
||||
.marker-swatch:hover .swatch-dot { transform: scale(1.15); }
|
||||
.marker-swatch-active .swatch-dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--swatch) 30%, transparent); transform: scale(1.1); }
|
||||
.swatch-label { font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); color: var(--text-faint); text-transform: capitalize; line-height: 1; }
|
||||
@@ -520,20 +636,48 @@
|
||||
.marker-cancel-btn { flex: 1; padding: 6px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); cursor: pointer; transition: color var(--t-base), border-color var(--t-base); text-align: center; }
|
||||
.marker-cancel-btn:hover { color: var(--text-muted); border-color: var(--border-strong); }
|
||||
|
||||
.wc-wrap { position: static; flex-shrink: 0; }
|
||||
.wc-clip { position: absolute; z-index: 100; }
|
||||
.wc-clip-bottom { top: 100%; right: var(--sp-3); clip-path: inset(0 -20px -20px -20px); }
|
||||
.wc-clip-right { left: calc(100% + 1px); top: auto; bottom: var(--sp-3); clip-path: inset(-20px -20px -20px 0); }
|
||||
.wc-clip-left { right: calc(100% + 1px); top: auto; bottom: var(--sp-3); clip-path: inset(-20px 0 -20px -20px); }
|
||||
.wc-bar { display: flex; align-items: center; gap: 1px; padding: 3px 10px 4px; background: var(--bg-raised); border: 1px solid var(--border-base); box-shadow: 0 6px 16px rgba(0,0,0,0.45); }
|
||||
.wc-clip-bottom .wc-bar { border-top: none; border-radius: 0 0 8px 8px; flex-direction: row; }
|
||||
.wc-clip-right .wc-bar { border-left: none; border-radius: 0 8px 8px 0; flex-direction: column; padding: 10px 4px; }
|
||||
.wc-clip-left .wc-bar { border-right: none; border-radius: 8px 0 0 8px; flex-direction: column; padding: 10px 4px; }
|
||||
.actions-wrap { position: relative; flex-shrink: 0; }
|
||||
.actions-popover {
|
||||
min-width: 160px;
|
||||
padding: var(--sp-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.wc-icon-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 24px; border-radius: var(--radius-sm); background: none; border: none; color: var(--text-muted); cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; }
|
||||
.wc-icon-btn:hover { color: var(--text-primary); background: var(--bg-overlay); }
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
width: 100%;
|
||||
padding: 7px var(--sp-2);
|
||||
border-radius: var(--radius-md);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background var(--t-fast), color var(--t-fast);
|
||||
}
|
||||
.action-row:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
||||
.action-row svg, .action-row :global(svg) { flex-shrink: 0; color: var(--text-faint); }
|
||||
.action-row:hover svg, .action-row:hover :global(svg) { color: var(--text-muted); }
|
||||
|
||||
.bar-middle { flex: 1; display: flex; flex-direction: column; align-items: center; width: 100%; min-height: 0; padding: var(--sp-1) 0; overflow: hidden; }
|
||||
.action-divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) 0; }
|
||||
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
||||
.dl-popover { min-width: 220px; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; }
|
||||
.dl-title { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 2px var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--border-dim); margin-bottom: var(--sp-1); }
|
||||
.dl-option { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; padding: 7px var(--sp-2); border-radius: var(--radius-md); font-size: var(--text-sm); color: var(--text-secondary); background: none; border: none; cursor: pointer; text-align: left; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.dl-option:hover:not(:disabled) { background: var(--bg-overlay); color: var(--text-primary); }
|
||||
.dl-option:disabled { opacity: 0.3; cursor: default; }
|
||||
.dl-sub { font-size: var(--text-xs); color: var(--text-faint); }
|
||||
.dl-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.dl-stepper { display: flex; align-items: center; gap: 2px; background: var(--bg-overlay); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; flex-shrink: 0; }
|
||||
.dl-step-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 28px; font-size: var(--text-base); color: var(--text-muted); background: none; border: none; cursor: pointer; line-height: 1; transition: color var(--t-fast), background var(--t-fast); }
|
||||
.dl-step-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.dl-step-btn:disabled { opacity: 0.25; cursor: default; }
|
||||
.dl-step-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); min-width: 24px; text-align: center; letter-spacing: var(--tracking-wide); }
|
||||
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.96) } to { opacity: 1; transform: scale(1) } }
|
||||
</style>
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { readerState } from "$lib/state/reader.svelte";
|
||||
import { getAdapter } from "$lib/request-manager";
|
||||
import type { Chapter } from "$lib/types";
|
||||
import { readerState } from "$lib/state/reader.svelte";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { getAdapter } from "$lib/request-manager";
|
||||
import type { Chapter } from "$lib/types";
|
||||
|
||||
interface Props {
|
||||
showResumeBanner: boolean;
|
||||
resumePage: number;
|
||||
resumeFading: boolean;
|
||||
adjacent: { remaining: Chapter[] };
|
||||
adjacent: { prev: Chapter | null; next: Chapter | null; remaining: Chapter[] };
|
||||
onDismissResume: () => void;
|
||||
barPosition: "top" | "left" | "right";
|
||||
}
|
||||
|
||||
const { showResumeBanner, resumePage, resumeFading, adjacent, onDismissResume }: Props = $props();
|
||||
const { showResumeBanner, resumePage, resumeFading, adjacent, onDismissResume, barPosition }: Props = $props();
|
||||
|
||||
async function gqlMutation(query: string, variables: Record<string, unknown>): Promise<void> {
|
||||
const base = settingsState.settings.serverUrl ?? "http://localhost:4567";
|
||||
@@ -53,7 +55,7 @@
|
||||
|
||||
{#if readerState.dlOpen && readerState.activeChapter}
|
||||
{@const chapter = readerState.activeChapter}
|
||||
<div class="dl-backdrop" role="presentation" onclick={() => readerState.dlOpen = false}>
|
||||
<div class="dl-backdrop dl-backdrop-{barPosition}" role="presentation" onclick={() => readerState.dlOpen = false}>
|
||||
<div class="dl-modal" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<p class="dl-title">Download</p>
|
||||
|
||||
@@ -91,8 +93,14 @@
|
||||
@keyframes bannerIn { from { opacity: 0; translate: -50% -6px; scale: 0.97; } to { opacity: 1; translate: -50% 0; scale: 1; } }
|
||||
@keyframes bannerOut { from { opacity: 1; translate: -50% 0; scale: 1; } to { opacity: 0; translate: -50% -4px; scale: 0.97; } }
|
||||
|
||||
.dl-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-reader) + 10); display: flex; align-items: flex-start; justify-content: flex-end; padding: 48px var(--sp-4) 0; }
|
||||
.dl-modal { background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-xl); padding: var(--sp-3); min-width: 210px; display: flex; flex-direction: column; gap: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.6); animation: scaleIn 0.12s ease both; transform-origin: top right; }
|
||||
.dl-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-reader) + 10); display: flex; padding: var(--sp-4); }
|
||||
.dl-backdrop-top { align-items: flex-start; justify-content: flex-end; padding-top: 52px; padding-right: var(--sp-4); }
|
||||
.dl-backdrop-left { align-items: flex-end; justify-content: flex-start; padding-bottom: var(--sp-4); padding-left: 52px; }
|
||||
.dl-backdrop-right { align-items: flex-end; justify-content: flex-end; padding-bottom: var(--sp-4); padding-right: 52px; }
|
||||
.dl-modal { background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-xl); padding: var(--sp-3); min-width: 210px; display: flex; flex-direction: column; gap: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.6); animation: scaleIn 0.12s ease both; }
|
||||
.dl-backdrop-top .dl-modal { transform-origin: top right; }
|
||||
.dl-backdrop-left .dl-modal { transform-origin: bottom left; }
|
||||
.dl-backdrop-right .dl-modal { transform-origin: bottom right; }
|
||||
.dl-title { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 2px var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--border-dim); margin-bottom: var(--sp-1); }
|
||||
.dl-option { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; padding: 7px var(--sp-3); border-radius: var(--radius-md); font-size: var(--text-sm); color: var(--text-secondary); background: none; border: none; cursor: pointer; text-align: left; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.dl-option:hover:not(:disabled) { background: var(--bg-overlay); color: var(--text-primary); }
|
||||
|
||||
@@ -402,7 +402,7 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: calc(var(--z-reader) + 20);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -412,11 +412,11 @@
|
||||
bottom: 0;
|
||||
width: 320px;
|
||||
z-index: calc(var(--z-reader) + 21);
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-base);
|
||||
background: var(--bg-void);
|
||||
border-left: 1px solid var(--border-dim);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -12px 0 40px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@@ -432,8 +432,8 @@
|
||||
.panel-title {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--weight-regular);
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
|
||||
@@ -505,12 +505,12 @@
|
||||
padding: var(--sp-2) var(--sp-1);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.option-tile:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.option-tile:hover { color: var(--text-secondary); background: var(--bg-overlay); border-color: var(--border-base); }
|
||||
.option-tile.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.tile-icon { display: flex; align-items: center; justify-content: center; }
|
||||
@@ -530,12 +530,12 @@
|
||||
padding: var(--sp-2) var(--sp-1);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.bar-tile:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.bar-tile:hover { color: var(--text-secondary); background: var(--bg-overlay); border-color: var(--border-base); }
|
||||
.bar-tile.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.bar-tile-preview {
|
||||
@@ -577,7 +577,7 @@
|
||||
|
||||
.toggle-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toggle-badge {
|
||||
@@ -629,19 +629,19 @@
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
font-size: var(--text-xs);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.dir-btn:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.dir-btn:hover { color: var(--text-secondary); background: var(--bg-overlay); border-color: var(--border-base); }
|
||||
.dir-btn.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.zoom-readout {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
@@ -659,14 +659,14 @@
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.zoom-step:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-step:hover:not(:disabled) { color: var(--text-secondary); background: var(--bg-overlay); }
|
||||
.zoom-step:disabled { opacity: 0.25; cursor: default; }
|
||||
|
||||
.zoom-slider {
|
||||
@@ -742,8 +742,8 @@
|
||||
|
||||
.preset-name {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-muted);
|
||||
font-weight: var(--weight-regular);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -34,22 +34,53 @@
|
||||
|
||||
const hValue = $derived(rtl ? sliderMax - sliderPage + 1 : sliderPage);
|
||||
const hPct = $derived(`--pct:${sliderPct}%`);
|
||||
const vPct = $derived(`--pct:${sliderPct}%`);
|
||||
|
||||
function handleH(e: Event) {
|
||||
const raw = Number((e.target as HTMLInputElement).value);
|
||||
onJumpToPage(rtl ? sliderMax - raw + 1 : raw);
|
||||
}
|
||||
|
||||
function handleV(e: Event) {
|
||||
onJumpToPage(Number((e.target as HTMLInputElement).value));
|
||||
}
|
||||
|
||||
function markerPct(pageNumber: number, forRtl = false): number {
|
||||
if (sliderMax <= 1) return 0;
|
||||
const ord = forRtl ? sliderMax - pageNumber + 1 : pageNumber;
|
||||
return ((ord - 1) / (sliderMax - 1)) * 100;
|
||||
}
|
||||
|
||||
// Custom vertical slider
|
||||
let trackEl = $state<HTMLDivElement | null>(null);
|
||||
let dragging = $state(false);
|
||||
|
||||
function pctFromPointer(clientY: number): number {
|
||||
if (!trackEl) return 0;
|
||||
const rect = trackEl.getBoundingClientRect();
|
||||
return Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
||||
}
|
||||
|
||||
function pageFromPct(pct: number): number {
|
||||
return Math.round(1 + pct * (sliderMax - 1));
|
||||
}
|
||||
|
||||
function handleTrackPointerDown(e: PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
dragging = true;
|
||||
readerState.sliderDragging = true;
|
||||
const pct = pctFromPointer(e.clientY);
|
||||
onJumpToPage(pageFromPct(pct));
|
||||
}
|
||||
|
||||
function handleTrackPointerMove(e: PointerEvent) {
|
||||
if (!dragging) return;
|
||||
const pct = pctFromPointer(e.clientY);
|
||||
onJumpToPage(pageFromPct(pct));
|
||||
}
|
||||
|
||||
function handleTrackPointerUp(e: PointerEvent) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
readerState.sliderDragging = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !isVertical}
|
||||
@@ -103,43 +134,52 @@
|
||||
<ArrowRight size={13} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="vbar-progress" class:hidden={!uiVisible} class:vbar-right={barPosition === "right"}>
|
||||
<div class="vbar-progress" class:hidden={!uiVisible}>
|
||||
{#if sliderMax > 1}
|
||||
<div
|
||||
class="vslider-wrap"
|
||||
bind:this={trackEl}
|
||||
role="slider"
|
||||
aria-valuenow={sliderPage}
|
||||
aria-valuemin={1}
|
||||
aria-valuemax={sliderMax}
|
||||
tabindex="0"
|
||||
onmouseenter={() => readerState.sliderHover = true}
|
||||
onmouseleave={() => readerState.sliderHover = false}
|
||||
onmouseleave={() => { if (!dragging) readerState.sliderHover = false; }}
|
||||
onpointerdown={handleTrackPointerDown}
|
||||
onpointermove={handleTrackPointerMove}
|
||||
onpointerup={handleTrackPointerUp}
|
||||
onpointercancel={handleTrackPointerUp}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
class="v-range"
|
||||
style={vPct}
|
||||
min={1}
|
||||
max={sliderMax}
|
||||
value={sliderPage}
|
||||
oninput={handleV}
|
||||
onmousedown={() => readerState.sliderDragging = true}
|
||||
onmouseup={() => readerState.sliderDragging = false}
|
||||
/>
|
||||
<div class="vtrack">
|
||||
<div class="vtrack-fill" style="height:{sliderPct}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="vslider-markers" aria-hidden="true">
|
||||
{#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
|
||||
{@const bPct = sliderMax > 1 ? ((currentBookmark.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
|
||||
<div class="vslider-checkpoint bookmark-checkpoint"
|
||||
style="top:{bPct}%"
|
||||
style="top:{markerPct(currentBookmark.pageNumber)}%"
|
||||
title="Bookmark: Page {currentBookmark.pageNumber}">
|
||||
</div>
|
||||
{/if}
|
||||
{#each activeChapterMarkers as m (m.id)}
|
||||
{@const mPct = sliderMax > 1 ? ((m.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
|
||||
<div class="vslider-checkpoint marker-checkpoint"
|
||||
style="top:{mPct}%;background:{MARKER_COLOR_HEX[m.color]}"
|
||||
style="top:{markerPct(m.pageNumber)}%;background:{MARKER_COLOR_HEX[m.color]}"
|
||||
title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}">
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="vthumb" style="top:{sliderPct}%" class:dragging></div>
|
||||
|
||||
{#if readerState.sliderHover || readerState.sliderDragging}
|
||||
<div class="vslider-tooltip" style="top:{sliderPct}%" class:tooltip-right={barPosition === "right"}>
|
||||
<div
|
||||
class="vslider-tooltip"
|
||||
class:tooltip-right={barPosition === "right"}
|
||||
style="top:{sliderPct}%"
|
||||
>
|
||||
{sliderPage} / {sliderMax}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -179,21 +219,95 @@
|
||||
.marker-checkpoint { opacity: 0.85; }
|
||||
.slider-tooltip { position: absolute; bottom: calc(100% + 2px); transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-sm); padding: 2px 6px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-secondary); white-space: nowrap; pointer-events: none; z-index: 10; letter-spacing: var(--tracking-wide); }
|
||||
|
||||
.vbar-progress { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; width: 100%; padding: var(--sp-2) 0; transition: opacity 0.25s ease; pointer-events: none; }
|
||||
|
||||
.vbar-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--sp-2) 0;
|
||||
transition: opacity 0.25s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.vbar-progress.hidden { opacity: 0; }
|
||||
|
||||
.vslider-wrap { flex: 1; position: relative; display: flex; flex-direction: column; align-items: center; width: 36px; pointer-events: all; margin: var(--sp-1) 0; }
|
||||
.vslider-wrap {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
.vslider-wrap:focus { outline: none; }
|
||||
|
||||
.v-range { -webkit-appearance: slider-vertical; appearance: slider-vertical; writing-mode: vertical-lr; direction: rtl; width: 34px; height: 100%; background: transparent; cursor: pointer; position: relative; z-index: 2; margin: 0; padding: 0; }
|
||||
.v-range::-webkit-slider-runnable-track { width: 5px; background: linear-gradient(to bottom, var(--accent-fg) var(--pct, 0%), var(--border-strong) var(--pct, 0%)); border-radius: 3px; transition: width 0.15s ease, background 0.05s linear; }
|
||||
.v-range:hover::-webkit-slider-runnable-track,
|
||||
.v-range:active::-webkit-slider-runnable-track { width: 7px; }
|
||||
.v-range::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent-fg); box-shadow: 0 0 0 2px rgba(0,0,0,0.5); margin-left: -4.5px; transition: transform var(--t-fast); }
|
||||
.v-range:hover::-webkit-slider-thumb,
|
||||
.v-range:active::-webkit-slider-thumb { transform: scale(1.3); }
|
||||
.vtrack {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--border-strong);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transition: width 0.15s ease;
|
||||
}
|
||||
.vslider-wrap:hover .vtrack { width: 6px; }
|
||||
|
||||
.vtrack-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--accent-fg);
|
||||
transition: height 0.05s linear;
|
||||
}
|
||||
|
||||
.vthumb {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-fg);
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
transition: transform var(--t-fast);
|
||||
}
|
||||
.vslider-wrap:hover .vthumb,
|
||||
.vthumb.dragging { transform: translate(-50%, -50%) scale(1.3); }
|
||||
|
||||
.vslider-markers { position: absolute; inset: 0; pointer-events: none; }
|
||||
.vslider-checkpoint { position: absolute; left: 50%; transform: translate(-50%, -50%); width: 12px; height: 5px; border-radius: 2px; }
|
||||
.vslider-tooltip { position: absolute; left: calc(100% + 6px); transform: translateY(-50%); background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-sm); padding: 2px 6px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-secondary); white-space: nowrap; pointer-events: none; z-index: 10; letter-spacing: var(--tracking-wide); }
|
||||
.vslider-tooltip.tooltip-right { left: auto; right: calc(100% + 6px); }
|
||||
.vslider-checkpoint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.vslider-tooltip {
|
||||
position: absolute;
|
||||
left: calc(100% + 8px);
|
||||
transform: translateY(-50%);
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 6px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.vslider-tooltip.tooltip-right { left: auto; right: calc(100% + 8px); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user