Fix: RTL Keybinds Issue & Progress Bar (Untested)

This commit is contained in:
Youwes09
2026-04-10 19:15:05 -05:00
parent 7917491389
commit 1a08d2415f
2 changed files with 88 additions and 82 deletions
+76 -70
View File
@@ -134,7 +134,8 @@
let markedRead = new Set<number>(); let markedRead = new Set<number>();
let appending = false; let appending = false;
let abortCtrl: AbortController | null = null; let abortCtrl: AbortController | null = null;
let hasNavigated = false; let hasNavigated = false;
let startAtLastPage = false;
let resumePage = $state(0); let resumePage = $state(0);
let resumeDismissed = $state(false); let resumeDismissed = $state(false);
let resumeTimer: ReturnType<typeof setTimeout> | null = null; let resumeTimer: ReturnType<typeof setTimeout> | null = null;
@@ -175,8 +176,8 @@
? (store.activeChapterList.find(c => c.id === visibleChapterId) ?? store.activeChapter) ? (store.activeChapterList.find(c => c.id === visibleChapterId) ?? store.activeChapter)
: store.activeChapter : store.activeChapter
); );
const currentBookmark = $derived(displayChapter ? store.bookmarks.find(b => b.chapterId === displayChapter!.id) : undefined); const currentBookmark = $derived(store.activeManga ? store.bookmarks.find(b => b.mangaId === store.activeManga!.id) : undefined);
const isBookmarked = $derived(!!currentBookmark && currentBookmark.pageNumber === store.pageNumber); const isBookmarked = $derived(!!currentBookmark && currentBookmark.chapterId === displayChapter?.id && currentBookmark.pageNumber === store.pageNumber);
const currentPageMarkers = $derived( const currentPageMarkers = $derived(
displayChapter ? store.getMarkersForPage(displayChapter.id, store.pageNumber) : [] displayChapter ? store.getMarkersForPage(displayChapter.id, store.pageNumber) : []
@@ -230,11 +231,12 @@
: [] : []
); );
const currentGroup = $derived( const currentGroup = $derived.by(() => {
style === "double" && pageGroups.length const group = style === "double" && pageGroups.length
? (pageGroups.find(g => g.includes(store.pageNumber)) ?? [store.pageNumber]) ? (pageGroups.find(g => g.includes(store.pageNumber)) ?? [store.pageNumber])
: [store.pageNumber] : [store.pageNumber];
); return rtl ? [...group].reverse() : group;
});
const sliderPage = $derived.by(() => { const sliderPage = $derived.by(() => {
if (style === "double" && pageGroups.length) { if (style === "double" && pageGroups.length) {
@@ -249,7 +251,8 @@
return lastPage || 1; 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(() => { $effect(() => {
const chapter = displayChapter; const chapter = displayChapter;
@@ -268,6 +271,8 @@
abortCtrl = ctrl; abortCtrl = ctrl;
hasNavigated = false; hasNavigated = false;
appending = false; appending = false;
const goToLast = startAtLastPage;
startAtLastPage = false;
markedRead = new Set(); markedRead = new Set();
loading = true; loading = true;
error = null; error = null;
@@ -292,7 +297,8 @@
const urls = await fetchPages(id, ctrl.signal, resumeTo > 1 ? resumeTo - 1 : 0); const urls = await fetchPages(id, ctrl.signal, resumeTo > 1 ? resumeTo - 1 : 0);
if (ctrl.signal.aborted) return; if (ctrl.signal.aborted) return;
store.pageUrls = urls; 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; pageReady = true;
loading = false; loading = false;
if (adjacent.next) fetchPages(adjacent.next.id).catch(() => {}); if (adjacent.next) fetchPages(adjacent.next.id).catch(() => {});
@@ -472,7 +478,7 @@
while (i <= snap.length) { while (i <= snap.length) {
const a = aspects[i - 1]; const a = aspects[i - 1];
if (a > 1.2 || i === snap.length) { groups.push([i++]); } 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; pageGroups = groups;
}); });
@@ -517,6 +523,8 @@
if (style === "longstrip" && visibleChapterId && chapterId !== visibleChapterId) return; if (style === "longstrip" && visibleChapterId && chapterId !== visibleChapterId) return;
addHistory({ mangaId, mangaTitle, thumbnailUrl: thumb, chapterId, chapterName, readAt: Date.now() }); addHistory({ mangaId, mangaTitle, thumbnailUrl: thumb, chapterId, chapterName, readAt: Date.now() });
if (autoBookmark) { 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 }); addBookmark({ mangaId, mangaTitle, thumbnailUrl: thumb, chapterId, chapterName, pageNumber: pageNum });
} }
if (style !== "longstrip" && store.settings.autoMarkRead && atLast) markChapterRead(chapterId); if (style !== "longstrip" && store.settings.autoMarkRead && atLast) markChapterRead(chapterId);
@@ -599,7 +607,7 @@
else closeReader(); else closeReader();
} else { } else {
if (gi > 0) store.pageNumber = pageGroups[gi - 1][0]; 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() { function goBack() {
if (loading) return; if (loading) return;
if (style === "longstrip") { if (style === "longstrip") {
if (adjacent.prev) openReader(adjacent.prev, store.activeChapterList); if (adjacent.prev) { startAtLastPage = true; openReader(adjacent.prev, store.activeChapterList); }
return; return;
} }
if (style === "double" && pageGroups.length) { advanceGroup(false); return; } if (style === "double" && pageGroups.length) { advanceGroup(false); return; }
@@ -639,7 +647,7 @@
if (store.pageNumber > 1) { if (store.pageNumber > 1) {
if (style === "fade") { animateFade(() => { store.pageNumber--; }); } if (style === "fade") { animateFade(() => { store.pageNumber--; }); }
else { 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); const goNext = $derived(rtl ? goBack : goForward);
@@ -684,6 +692,8 @@
removeBookmark(ch.id); removeBookmark(ch.id);
resumeVisible = false; resumeVisible = false;
} else { } 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 }); 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); adjustZoom(e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP);
} }
function onSliderInput(e: Event) {
jumpToPage(Number((e.currentTarget as HTMLInputElement).value));
}
function onKey(e: KeyboardEvent) { function onKey(e: KeyboardEvent) {
if ((e.target as HTMLElement).tagName === "INPUT" || (e.target as HTMLElement).tagName === "TEXTAREA") return; if ((e.target as HTMLElement).tagName === "INPUT" || (e.target as HTMLElement).tagName === "TEXTAREA") return;
const kb = store.settings.keybinds ?? DEFAULT_KEYBINDS; const kb = store.settings.keybinds ?? DEFAULT_KEYBINDS;
const r = store.settings.readingDirection === "rtl";
if (e.key === "Escape") { if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
if (markerOpen) { markerOpen = false; return; } 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 === "-") { e.preventDefault(); adjustZoom(-ZOOM_STEP * 2); return; }
if (e.ctrlKey && e.key === "0") { e.preventDefault(); resetZoom(); return; } if (e.ctrlKey && e.key === "0") { e.preventDefault(); resetZoom(); return; }
if (matchesKeybind(e, kb.exitReader)) { e.preventDefault(); closeReader(); } if (matchesKeybind(e, kb.exitReader)) { e.preventDefault(); closeReader(); }
else if (matchesKeybind(e, kb.pageRight)) { e.preventDefault(); goForward(); } else if (matchesKeybind(e, kb.turnPageRight)) { e.preventDefault(); goNext(); }
else if (matchesKeybind(e, kb.pageLeft)) { e.preventDefault(); goBack(); } 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.firstPage)) { e.preventDefault(); store.pageNumber = 1; }
else if (matchesKeybind(e, kb.lastPage)) { e.preventDefault(); store.pageNumber = lastPage; } 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(); e.preventDefault();
const list = store.activeChapterList; const ch = rtl ? adjacent.prev : adjacent.next;
const idx = list.findIndex(c => c.id === store.activeChapter?.id); if (ch) { maybeMarkCurrentRead(); openReader(ch, store.activeChapterList); }
const next = idx >= 0 && idx < list.length - 1 ? list[idx + 1] : null;
if (next) { maybeMarkCurrentRead(); openReader(next, list); }
} }
else if (matchesKeybind(e, kb.chapterLeft)) { else if (matchesKeybind(e, kb.turnChapterLeft)) {
e.preventDefault(); e.preventDefault();
const list = store.activeChapterList; const ch = rtl ? adjacent.next : adjacent.prev;
const idx = list.findIndex(c => c.id === store.activeChapter?.id); if (ch) openReader(ch, store.activeChapterList);
const prev = idx > 0 ? list[idx - 1] : null;
if (prev) openReader(prev, list);
} }
else if (matchesKeybind(e, kb.togglePageStyle)) { e.preventDefault(); cycleStyle(); } 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.toggleFullscreen)) { e.preventDefault(); toggleFullscreen().catch(console.error); }
else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); setSettingsOpen(true); } else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); setSettingsOpen(true); }
else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); toggleBookmark(); } else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); toggleBookmark(); }
@@ -814,8 +815,7 @@
function handleTap(e: MouseEvent) { function handleTap(e: MouseEvent) {
if (style === "longstrip") return; if (style === "longstrip") return;
const x = e.clientX / window.innerWidth; const x = e.clientX / window.innerWidth;
if (!rtl) { if (x > 0.6) goForward(); else if (x < 0.4) goBack(); } if (x > 0.6) goNext(); else if (x < 0.4) goPrev();
else { if (x < 0.4) goForward(); else if (x > 0.6) goBack(); }
} }
async function runDl(fn: () => Promise<unknown>) { async function runDl(fn: () => Promise<unknown>) {
@@ -830,12 +830,18 @@
window.addEventListener("wheel", onWheel, { passive: false }); window.addEventListener("wheel", onWheel, { passive: false });
containerEl?.focus({ preventScroll: true }); containerEl?.focus({ preventScroll: true });
const ro = new ResizeObserver(entries => { containerWidth = entries[0].contentRect.width; }); let roTimer: ReturnType<typeof setTimeout> | 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); ro.observe(containerEl);
return () => { return () => {
abortCtrl?.abort(); abortCtrl?.abort();
if (hideTimer) clearTimeout(hideTimer); if (hideTimer) clearTimeout(hideTimer);
if (roTimer) clearTimeout(roTimer);
window.removeEventListener("keydown", onKey); window.removeEventListener("keydown", onKey);
window.removeEventListener("wheel", onWheel); window.removeEventListener("wheel", onWheel);
cleanupScroll(); cleanupScroll();
@@ -1081,49 +1087,48 @@
<div <div
class="slider-wrap" class="slider-wrap"
class:dragging={sliderDragging} class:dragging={sliderDragging}
role="slider"
aria-valuenow={sliderPage}
aria-valuemin={1}
aria-valuemax={sliderMax}
tabindex="-1"
onmouseenter={() => sliderHover = true} onmouseenter={() => sliderHover = true}
onmouseleave={() => { sliderHover = false; }} onmouseleave={() => { sliderHover = false; sliderDragging = false; }}
role="presentation" 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}
> >
<div class="slider-track-bg"> <div class="slider-track-bg">
<div class="slider-fill" style="width: {rtl ? 100 - sliderPct : sliderPct}%"></div> <div class="slider-fill" style={rtl ? `width:${100 - sliderPct}%;margin-left:auto` : `width:${sliderPct}%`}></div>
</div> </div>
<div class="slider-thumb" style="left:{sliderPct}%"></div>
{#if isBookmarked && currentBookmark}
{@const bPct = sliderMax > 1 ? ((currentBookmark.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
<div {#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
class="slider-checkpoint bookmark-checkpoint" {@const bOrd = rtl ? lastPage - currentBookmark.pageNumber + 1 : currentBookmark.pageNumber}
style="left: {rtl ? 100 - bPct : bPct}%" {@const bPct = lastPage > 1 ? ((bOrd - 1) / (lastPage - 1)) * 100 : 0}
title="Bookmark: Page {currentBookmark.pageNumber}" <div class="slider-checkpoint bookmark-checkpoint" style="left: {bPct}%" title="Bookmark: Page {currentBookmark.pageNumber}"></div>
></div>
{/if} {/if}
{#each activeChapterMarkers as m (m.id)} {#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}
<div {@const mPct = lastPage > 1 ? ((mOrd - 1) / (lastPage - 1)) * 100 : 0}
class="slider-checkpoint marker-checkpoint" <div class="slider-checkpoint marker-checkpoint" style="left: {mPct}%; background:{MARKER_COLOR_HEX[m.color]}" title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}"></div>
style="left: {rtl ? 100 - mPct : mPct}%; background:{MARKER_COLOR_HEX[m.color]}"
title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}"
></div>
{/each} {/each}
<input
type="range"
class="slider-input"
min={1}
max={sliderMax}
step={1}
value={sliderPage}
dir={rtl ? "rtl" : "ltr"}
oninput={onSliderInput}
onmousedown={() => sliderDragging = true}
onmouseup={() => sliderDragging = false}
ontouchstart={() => sliderDragging = true}
ontouchend={() => sliderDragging = false}
aria-label="Page {sliderPage} of {sliderMax}"
/>
{#if sliderHover || sliderDragging} {#if sliderHover || sliderDragging}
<div class="slider-tooltip" style="left: {rtl ? 100 - sliderPct : sliderPct}%"> <div class="slider-tooltip" style="left: {sliderPct}%">
{sliderPage} / {sliderMax} {sliderPage} / {sliderMax}
</div> </div>
{/if} {/if}
@@ -1257,11 +1262,12 @@
.nav-btn:disabled { opacity: 0.25; cursor: default; } .nav-btn:disabled { opacity: 0.25; cursor: default; }
.slider-wrap { flex: 1; position: relative; display: flex; align-items: center; height: 34px; cursor: pointer; } .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-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; } .slider-fill { height: 100%; background: var(--accent-fg); border-radius: 2px; transition: width 0.05s linear; position: relative; }
.slider-input { position: absolute; left: 0; right: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; z-index: 2; }
.slider-checkpoint { position: absolute; top: 50%; width: 4px; height: 10px; border-radius: 2px; transform: translate(-50%, -50%); pointer-events: none; z-index: 1; } .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; } .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-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; } .slider-wrap:hover .slider-track-bg, .slider-wrap.dragging .slider-track-bg { height: 5px; }
+12 -12
View File
@@ -1,12 +1,12 @@
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
export interface Keybinds { export interface Keybinds {
pageRight: string; turnPageRight: string;
pageLeft: string; turnPageLeft: string;
firstPage: string; firstPage: string;
lastPage: string; lastPage: string;
chapterRight: string; turnChapterRight: string;
chapterLeft: string; turnChapterLeft: string;
exitReader: string; exitReader: string;
toggleReadingDirection: string; toggleReadingDirection: string;
togglePageStyle: string; togglePageStyle: string;
@@ -17,12 +17,12 @@ export interface Keybinds {
} }
export const DEFAULT_KEYBINDS: Keybinds = { export const DEFAULT_KEYBINDS: Keybinds = {
pageRight: "ArrowRight", turnPageRight: "ArrowRight",
pageLeft: "ArrowLeft", turnPageLeft: "ArrowLeft",
firstPage: "ctrl+ArrowLeft", firstPage: "ctrl+ArrowLeft",
lastPage: "ctrl+ArrowRight", lastPage: "ctrl+ArrowRight",
chapterRight: "]", turnChapterRight: "]",
chapterLeft: "[", turnChapterLeft: "[",
exitReader: "Backspace", exitReader: "Backspace",
toggleReadingDirection: "d", toggleReadingDirection: "d",
togglePageStyle: "q", togglePageStyle: "q",
@@ -33,12 +33,12 @@ export const DEFAULT_KEYBINDS: Keybinds = {
}; };
export const KEYBIND_LABELS: Record<keyof Keybinds, string> = { export const KEYBIND_LABELS: Record<keyof Keybinds, string> = {
pageRight: "Turn page right", turnPageRight: "Turn page right (→)",
pageLeft: "Turn page left", turnPageLeft: "Turn page left (←)",
firstPage: "Jump to first page", firstPage: "Jump to first page",
lastPage: "Jump to last page", lastPage: "Jump to last page",
chapterRight: "Next chapter", turnChapterRight: "Turn chapter right (→)",
chapterLeft: "Previous chapter", turnChapterLeft: "Turn chapter left (←)",
exitReader: "Exit reader", exitReader: "Exit reader",
toggleReadingDirection: "Toggle reading direction", toggleReadingDirection: "Toggle reading direction",
togglePageStyle: "Toggle page style", togglePageStyle: "Toggle page style",