From 1a08d2415fbd3ec702261268ef86fe0a7bb0eead Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Fri, 10 Apr 2026 19:15:05 -0500 Subject: [PATCH] Fix: RTL Keybinds Issue & Progress Bar (Untested) --- src/components/reader/Reader.svelte | 146 +++++++++++++++------------- src/lib/keybinds.ts | 24 ++--- 2 files changed, 88 insertions(+), 82 deletions(-) diff --git a/src/components/reader/Reader.svelte b/src/components/reader/Reader.svelte index 06f4a33..ab66a70 100644 --- a/src/components/reader/Reader.svelte +++ b/src/components/reader/Reader.svelte @@ -134,7 +134,8 @@ let markedRead = new Set(); let appending = false; let abortCtrl: AbortController | null = null; - let hasNavigated = false; + let hasNavigated = false; + let startAtLastPage = false; let resumePage = $state(0); let resumeDismissed = $state(false); let resumeTimer: ReturnType | null = null; @@ -175,8 +176,8 @@ ? (store.activeChapterList.find(c => c.id === visibleChapterId) ?? store.activeChapter) : store.activeChapter ); - const currentBookmark = $derived(displayChapter ? store.bookmarks.find(b => b.chapterId === displayChapter!.id) : undefined); - const isBookmarked = $derived(!!currentBookmark && currentBookmark.pageNumber === store.pageNumber); + const currentBookmark = $derived(store.activeManga ? store.bookmarks.find(b => b.mangaId === store.activeManga!.id) : undefined); + const isBookmarked = $derived(!!currentBookmark && currentBookmark.chapterId === displayChapter?.id && currentBookmark.pageNumber === store.pageNumber); const currentPageMarkers = $derived( displayChapter ? store.getMarkersForPage(displayChapter.id, store.pageNumber) : [] @@ -230,11 +231,12 @@ : [] ); - const currentGroup = $derived( - style === "double" && pageGroups.length + const currentGroup = $derived.by(() => { + const group = style === "double" && pageGroups.length ? (pageGroups.find(g => g.includes(store.pageNumber)) ?? [store.pageNumber]) - : [store.pageNumber] - ); + : [store.pageNumber]; + return rtl ? [...group].reverse() : group; + }); const sliderPage = $derived.by(() => { if (style === "double" && pageGroups.length) { @@ -249,7 +251,8 @@ return lastPage || 1; }); - const sliderPct = $derived(sliderMax > 1 ? ((sliderPage - 1) / (sliderMax - 1)) * 100 : 0); + const sliderPctRaw = $derived(sliderMax > 1 ? ((sliderPage - 1) / (sliderMax - 1)) * 100 : 0); + const sliderPct = $derived(rtl ? 100 - sliderPctRaw : sliderPctRaw); $effect(() => { const chapter = displayChapter; @@ -268,6 +271,8 @@ abortCtrl = ctrl; hasNavigated = false; appending = false; + const goToLast = startAtLastPage; + startAtLastPage = false; markedRead = new Set(); loading = true; error = null; @@ -292,7 +297,8 @@ const urls = await fetchPages(id, ctrl.signal, resumeTo > 1 ? resumeTo - 1 : 0); if (ctrl.signal.aborted) return; store.pageUrls = urls; - if (resumeTo > 1) store.pageNumber = Math.min(resumeTo, urls.length || resumeTo); + if (goToLast) store.pageNumber = urls.length; + else if (resumeTo > 1) store.pageNumber = Math.min(resumeTo, urls.length || resumeTo); pageReady = true; loading = false; if (adjacent.next) fetchPages(adjacent.next.id).catch(() => {}); @@ -472,7 +478,7 @@ while (i <= snap.length) { const a = aspects[i - 1]; if (a > 1.2 || i === snap.length) { groups.push([i++]); } - else { groups.push(rtl ? [i + 1, i] : [i, i + 1]); i += 2; } + else { groups.push([i, i + 1]); i += 2; } } pageGroups = groups; }); @@ -517,6 +523,8 @@ if (style === "longstrip" && visibleChapterId && chapterId !== visibleChapterId) return; addHistory({ mangaId, mangaTitle, thumbnailUrl: thumb, chapterId, chapterName, readAt: Date.now() }); if (autoBookmark) { + const existing = store.bookmarks.find(b => b.mangaId === mangaId && b.chapterId !== chapterId); + if (existing) removeBookmark(existing.chapterId); addBookmark({ mangaId, mangaTitle, thumbnailUrl: thumb, chapterId, chapterName, pageNumber: pageNum }); } if (style !== "longstrip" && store.settings.autoMarkRead && atLast) markChapterRead(chapterId); @@ -599,7 +607,7 @@ else closeReader(); } else { if (gi > 0) store.pageNumber = pageGroups[gi - 1][0]; - else if (adjacent.prev) openReader(adjacent.prev, store.activeChapterList); + else if (adjacent.prev) { startAtLastPage = true; openReader(adjacent.prev, store.activeChapterList); } } } @@ -631,7 +639,7 @@ function goBack() { if (loading) return; if (style === "longstrip") { - if (adjacent.prev) openReader(adjacent.prev, store.activeChapterList); + if (adjacent.prev) { startAtLastPage = true; openReader(adjacent.prev, store.activeChapterList); } return; } if (style === "double" && pageGroups.length) { advanceGroup(false); return; } @@ -639,7 +647,7 @@ if (store.pageNumber > 1) { if (style === "fade") { animateFade(() => { store.pageNumber--; }); } else { store.pageNumber--; } - } else if (adjacent.prev) { openReader(adjacent.prev, store.activeChapterList); } + } else if (adjacent.prev) { startAtLastPage = true; openReader(adjacent.prev, store.activeChapterList); } } const goNext = $derived(rtl ? goBack : goForward); @@ -684,6 +692,8 @@ removeBookmark(ch.id); resumeVisible = false; } else { + const existing = store.bookmarks.find(b => b.mangaId === manga.id && b.chapterId !== ch.id); + if (existing) removeBookmark(existing.chapterId); addBookmark({ mangaId: manga.id, mangaTitle: manga.title, thumbnailUrl: manga.thumbnailUrl, chapterId: ch.id, chapterName: ch.name, pageNumber: store.pageNumber }); } } @@ -766,14 +776,9 @@ adjustZoom(e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP); } - function onSliderInput(e: Event) { - jumpToPage(Number((e.currentTarget as HTMLInputElement).value)); - } - function onKey(e: KeyboardEvent) { if ((e.target as HTMLElement).tagName === "INPUT" || (e.target as HTMLElement).tagName === "TEXTAREA") return; const kb = store.settings.keybinds ?? DEFAULT_KEYBINDS; - const r = store.settings.readingDirection === "rtl"; if (e.key === "Escape") { e.preventDefault(); if (markerOpen) { markerOpen = false; return; } @@ -785,26 +790,22 @@ if (e.ctrlKey && e.key === "-") { e.preventDefault(); adjustZoom(-ZOOM_STEP * 2); return; } if (e.ctrlKey && e.key === "0") { e.preventDefault(); resetZoom(); return; } if (matchesKeybind(e, kb.exitReader)) { e.preventDefault(); closeReader(); } - else if (matchesKeybind(e, kb.pageRight)) { e.preventDefault(); goForward(); } - else if (matchesKeybind(e, kb.pageLeft)) { e.preventDefault(); goBack(); } + else if (matchesKeybind(e, kb.turnPageRight)) { e.preventDefault(); goNext(); } + else if (matchesKeybind(e, kb.turnPageLeft)) { e.preventDefault(); goPrev(); } else if (matchesKeybind(e, kb.firstPage)) { e.preventDefault(); store.pageNumber = 1; } else if (matchesKeybind(e, kb.lastPage)) { e.preventDefault(); store.pageNumber = lastPage; } - else if (matchesKeybind(e, kb.chapterRight)) { + else if (matchesKeybind(e, kb.turnChapterRight)) { e.preventDefault(); - const list = store.activeChapterList; - const idx = list.findIndex(c => c.id === store.activeChapter?.id); - const next = idx >= 0 && idx < list.length - 1 ? list[idx + 1] : null; - if (next) { maybeMarkCurrentRead(); openReader(next, list); } + const ch = rtl ? adjacent.prev : adjacent.next; + if (ch) { maybeMarkCurrentRead(); openReader(ch, store.activeChapterList); } } - else if (matchesKeybind(e, kb.chapterLeft)) { + else if (matchesKeybind(e, kb.turnChapterLeft)) { e.preventDefault(); - const list = store.activeChapterList; - const idx = list.findIndex(c => c.id === store.activeChapter?.id); - const prev = idx > 0 ? list[idx - 1] : null; - if (prev) openReader(prev, list); + const ch = rtl ? adjacent.next : adjacent.prev; + if (ch) openReader(ch, store.activeChapterList); } else if (matchesKeybind(e, kb.togglePageStyle)) { e.preventDefault(); cycleStyle(); } - else if (matchesKeybind(e, kb.toggleReadingDirection)) { e.preventDefault(); updateSettings({ readingDirection: r ? "ltr" : "rtl" }); } + else if (matchesKeybind(e, kb.toggleReadingDirection)) { e.preventDefault(); updateSettings({ readingDirection: rtl ? "ltr" : "rtl" }); } else if (matchesKeybind(e, kb.toggleFullscreen)) { e.preventDefault(); toggleFullscreen().catch(console.error); } else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); setSettingsOpen(true); } else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); toggleBookmark(); } @@ -814,8 +815,7 @@ function handleTap(e: MouseEvent) { if (style === "longstrip") return; const x = e.clientX / window.innerWidth; - if (!rtl) { if (x > 0.6) goForward(); else if (x < 0.4) goBack(); } - else { if (x < 0.4) goForward(); else if (x > 0.6) goBack(); } + if (x > 0.6) goNext(); else if (x < 0.4) goPrev(); } async function runDl(fn: () => Promise) { @@ -830,12 +830,18 @@ window.addEventListener("wheel", onWheel, { passive: false }); containerEl?.focus({ preventScroll: true }); - const ro = new ResizeObserver(entries => { containerWidth = entries[0].contentRect.width; }); + let roTimer: ReturnType | null = null; + const ro = new ResizeObserver(entries => { + const w = entries[0].contentRect.width; + if (roTimer) clearTimeout(roTimer); + roTimer = setTimeout(() => { containerWidth = w; roTimer = null; }, 50); + }); ro.observe(containerEl); return () => { abortCtrl?.abort(); if (hideTimer) clearTimeout(hideTimer); + if (roTimer) clearTimeout(roTimer); window.removeEventListener("keydown", onKey); window.removeEventListener("wheel", onWheel); cleanupScroll(); @@ -1081,49 +1087,48 @@
sliderHover = true} - onmouseleave={() => { sliderHover = false; }} - role="presentation" + onmouseleave={() => { sliderHover = false; sliderDragging = false; }} + onmousedown={(e) => { + sliderDragging = true; + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + jumpToPage(Math.round(1 + (rtl ? 1 - ratio : ratio) * (sliderMax - 1))); + }} + onmousemove={(e) => { + if (!sliderDragging) return; + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + jumpToPage(Math.round(1 + (rtl ? 1 - ratio : ratio) * (sliderMax - 1))); + }} + onmouseup={() => sliderDragging = false} >
-
+
+
- {#if isBookmarked && currentBookmark} - {@const bPct = sliderMax > 1 ? ((currentBookmark.pageNumber - 1) / (sliderMax - 1)) * 100 : 0} -
+ + + {#if currentBookmark && currentBookmark.chapterId === displayChapter?.id} + {@const bOrd = rtl ? lastPage - currentBookmark.pageNumber + 1 : currentBookmark.pageNumber} + {@const bPct = lastPage > 1 ? ((bOrd - 1) / (lastPage - 1)) * 100 : 0} +
{/if} {#each activeChapterMarkers as m (m.id)} - {@const mPct = sliderMax > 1 ? ((m.pageNumber - 1) / (sliderMax - 1)) * 100 : 0} -
+ {@const mOrd = rtl ? lastPage - m.pageNumber + 1 : m.pageNumber} + {@const mPct = lastPage > 1 ? ((mOrd - 1) / (lastPage - 1)) * 100 : 0} +
{/each} - sliderDragging = true} - onmouseup={() => sliderDragging = false} - ontouchstart={() => sliderDragging = true} - ontouchend={() => sliderDragging = false} - aria-label="Page {sliderPage} of {sliderMax}" - /> {#if sliderHover || sliderDragging} -
+
{sliderPage} / {sliderMax}
{/if} @@ -1257,11 +1262,12 @@ .nav-btn:disabled { opacity: 0.25; cursor: default; } .slider-wrap { flex: 1; position: relative; display: flex; align-items: center; height: 34px; cursor: pointer; } - .slider-track-bg { position: absolute; left: 0; right: 0; height: 3px; background: var(--border-strong); border-radius: 2px; overflow: hidden; pointer-events: none; } - .slider-fill { height: 100%; background: var(--accent-fg); border-radius: 2px; transition: width 0.05s linear; } - .slider-input { position: absolute; left: 0; right: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; z-index: 2; } + .slider-track-bg { position: absolute; left: 0; right: 0; height: 3px; background: var(--border-strong); border-radius: 2px; pointer-events: none; } + .slider-fill { height: 100%; background: var(--accent-fg); border-radius: 2px; transition: width 0.05s linear; position: relative; } .slider-checkpoint { position: absolute; top: 50%; width: 4px; height: 10px; border-radius: 2px; transform: translate(-50%, -50%); pointer-events: none; z-index: 1; } - .bookmark-checkpoint { background: var(--accent-fg); opacity: 0.7; } + .slider-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%); width: 12px; height: 12px; border-radius: 50%; background: var(--accent-fg); pointer-events: none; z-index: 2; box-shadow: 0 0 0 2px rgba(0,0,0,0.5); transition: transform var(--t-fast); } + .slider-wrap:hover .slider-thumb, .slider-wrap.dragging .slider-thumb { transform: translate(-50%, -50%) scale(1.3); } + .bookmark-checkpoint { background: #ffffff; opacity: 0.8; } .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); } .slider-wrap:hover .slider-track-bg, .slider-wrap.dragging .slider-track-bg { height: 5px; } diff --git a/src/lib/keybinds.ts b/src/lib/keybinds.ts index 51e1353..8823f99 100644 --- a/src/lib/keybinds.ts +++ b/src/lib/keybinds.ts @@ -1,12 +1,12 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; export interface Keybinds { - pageRight: string; - pageLeft: string; + turnPageRight: string; + turnPageLeft: string; firstPage: string; lastPage: string; - chapterRight: string; - chapterLeft: string; + turnChapterRight: string; + turnChapterLeft: string; exitReader: string; toggleReadingDirection: string; togglePageStyle: string; @@ -17,12 +17,12 @@ export interface Keybinds { } export const DEFAULT_KEYBINDS: Keybinds = { - pageRight: "ArrowRight", - pageLeft: "ArrowLeft", + turnPageRight: "ArrowRight", + turnPageLeft: "ArrowLeft", firstPage: "ctrl+ArrowLeft", lastPage: "ctrl+ArrowRight", - chapterRight: "]", - chapterLeft: "[", + turnChapterRight: "]", + turnChapterLeft: "[", exitReader: "Backspace", toggleReadingDirection: "d", togglePageStyle: "q", @@ -33,12 +33,12 @@ export const DEFAULT_KEYBINDS: Keybinds = { }; export const KEYBIND_LABELS: Record = { - pageRight: "Turn page right", - pageLeft: "Turn page left", + turnPageRight: "Turn page right (→)", + turnPageLeft: "Turn page left (←)", firstPage: "Jump to first page", lastPage: "Jump to last page", - chapterRight: "Next chapter", - chapterLeft: "Previous chapter", + turnChapterRight: "Turn chapter right (→)", + turnChapterLeft: "Turn chapter left (←)", exitReader: "Exit reader", toggleReadingDirection: "Toggle reading direction", togglePageStyle: "Toggle page style",