-
${t('reader.sync_dlg_title')}
+
${t('reader.sync_dlg_title')}
+
${t('reader.sync_dlg_hint')}
@@ -3915,12 +4226,12 @@ function showSyncDialog(best, localPct, localTime) {
- | ${best.device || 'KOReader'} ${rNewer ? '★' : ''} |
+ ${best.device || 'KOReader'} ${rNewer ? '▲' : ''} |
${rPct}% |
${rDate} |
- | ${t('reader.sync_dlg_this_reader')} ${!rNewer ? '★' : ''} |
+ ${t('reader.sync_dlg_this_reader')} ${!rNewer ? '▲' : ''} |
${lPct}% |
${lDate} |
@@ -3935,7 +4246,8 @@ function showSyncDialog(best, localPct, localTime) {
const close = (ok) => { backdrop.remove(); resolve(ok); };
backdrop.querySelector('#sync-dlg-yes').addEventListener('click', () => close(true));
backdrop.querySelector('#sync-dlg-ignore').addEventListener('click', () => close(false));
- backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(false); });
+ // Backdrop click defaults to Jump (the forward/safe direction — never accidentally go backwards)
+ backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(true); });
});
}
@@ -3986,57 +4298,21 @@ async function syncOnOpen(localProgress) {
console.log('[kosync] xpointerMatch:', xpointerMatch, 'local:', localXPointer, 'remote:', best.progress);
const pctDiffers = Math.abs((best.percentage || 0) - localPct) > 0.01;
- if (!xpointerMatch && pctDiffers) {
+ // Never silently jump backwards — only prompt when the remote is ahead.
+ // If remote is behind local, it means we already synced more recently from this
+ // device (e.g. the hide-beacon fired but localProgress hasn't updated yet).
+ const remoteIsAhead = (best.percentage || 0) > localPct + 0.005;
+ if (!xpointerMatch && pctDiffers && remoteIsAhead) {
const doSync = await showSyncDialog(best, localPct, localTime);
- // Return both percentage and the xpointer string so the caller can navigate
- // directly to a spine item when the xpointer is a DocFragment-only reference.
if (doSync) return { percentage: best.percentage, progress: best.progress };
}
return null;
}
-// Called by the Android app when the network becomes available (wake from standby /
-// reconnect). Re-runs the same sync-on-open flow so the reader can jump to a position
-// advanced on another device while this one was offline / sleeping.
-async function networkRestoreSync() {
- if (!currentBook || !isReady) return;
- try {
- const localProgress = await apiFetch(`/progress/${currentBook.file_hash}`).catch(() => null);
- const syncTarget = await syncOnOpen(localProgress);
- if (!syncTarget?.percentage != null && !syncTarget?.progress) return;
- // Reuse the same navigation logic used at book-open time
- const dfMatch = syncTarget.progress?.match(/^\/body\/DocFragment\[(\d+)\]/);
- if (dfMatch) {
- const spineIdx = parseInt(dfMatch[1]) - 1;
- const spineItem = book.spine.get(spineIdx);
- if (spineItem?.href) {
- const paraMatch = !prefs.bionicReading && syncTarget.progress.match(/\/p\[(\d+)\]/);
- let navigated = false;
- if (paraMatch) {
- const guessCfi = `epubcfi(/6/${(spineIdx + 1) * 2}!/4/${parseInt(paraMatch[1]) * 2})`;
- try { await rendition.display(guessCfi); navigated = true; } catch { navigated = false; }
- }
- if (!navigated) {
- await rendition.display(spineItem.href);
- if (prefs.bionicReading && book.locations.length() > 0)
- await seekToPercentage(syncTarget.percentage);
- }
- } else if (book.locations.length() > 0) {
- await seekToPercentage(syncTarget.percentage);
- }
- } else if (syncTarget?.percentage != null) {
- if (book.locations.length() > 0)
- await seekToPercentage(syncTarget.percentage);
- }
- } catch (e) { console.warn('[kosync] networkRestoreSync failed:', e.message); }
-}
-window.__codexaNetworkRestore = networkRestoreSync;
-
window.addEventListener('online', () => {
if (!currentBook) return;
syncOfflineBookmarks(currentBook.id).catch(() => {});
syncOfflineAnnotations(currentBook.id).catch(() => {});
- networkRestoreSync().catch(() => {});
});
@@ -4180,22 +4456,34 @@ function goNext() {
function goPrev() {
deferredNextPending = false;
console.log(`[nav] PREV | page=${currentChapPage}/${currentChapTotal}`);
- pendingNavDirection = 'prev'; sessionPageCount++; rendition?.prev();
+ pendingNavDirection = 'prev';
+ sessionPageCount++;
+ rendition?.prev();
}
// ── Touch / swipe navigation ──────────────────────────────────────────────────
const SWIPE_THRESHOLD = 24; // min px horizontal distance
const SWIPE_MAX_VERT = 130; // max vertical drift allowed
const TAP_MAX_DRIFT = 20; // max px movement still counted as a tap
-const TOP_REVEAL_ZONE = 92; // px from top where tap reveals header
const SWIPE_DOWN_OPEN = 42; // px downward swipe to reveal header
const SWIPE_UP_CLOSE = 30; // px upward swipe to hide header when open
// iOS detection (Chrome/Safari on iPhone/iPad use WebKit with different iframe touch behaviour)
const isIOS = /iP(hone|od|ad)/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
+
+/** Phone / tablet / PWA touch device. */
+function isTouchReader() {
+ return isIOS ||
+ isAndroidApp() ||
+ navigator.maxTouchPoints > 0 ||
+ !!window.matchMedia?.('(pointer: coarse)')?.matches;
+}
+
+
let touchStartX = 0;
let touchStartY = 0;
let suppressNextTap = false; // set by long-press dict lookup to prevent navigation on touchend
+let _selectSuppressNav = false; // true while a text-selection gesture is active — blocks swipe-nav preventDefault
function handleTouchStart(e) {
touchStartX = e.changedTouches[0].clientX;
@@ -4209,25 +4497,30 @@ function handleTouchEnd(e) {
const absDy = Math.abs(dy);
const y = e.changedTouches[0].clientY;
if (prefs.autoHideHeader && dy > SWIPE_DOWN_OPEN && absDx < 70) {
+ const wasHidden = !readerLayout.classList.contains('header-peek');
if (!readerLayout.classList.toggle('header-peek')) closeJumpPanel();
+ else if (wasHidden) _headerRevealTs = Date.now();
+ syncHeaderDismissBackdrop();
return;
}
if (prefs.autoHideHeader && dy < -SWIPE_UP_CLOSE && absDx < 70 && readerLayout.classList.contains('header-peek')) {
- readerLayout.classList.remove('header-peek');
+ forceHideAutoHeader();
closeJumpPanel();
return;
}
if (absDx < TAP_MAX_DRIFT && absDy < TAP_MAX_DRIFT) {
- if (prefs.autoHideHeader && y < TOP_REVEAL_ZONE + 20) {
- readerLayout.classList.add('header-peek');
+ const x = e.changedTouches[0].clientX;
+ if (prefs.autoHideHeader && inHeaderRevealZone(y)) {
+ revealHeader();
+ return;
+ }
+ const nav = inNavZone(x);
+ if (nav === 'prev') { goPrev(); return; }
+ if (nav === 'next') { goNext(); return; }
+ if (prefs.autoHideHeader && readerLayout.classList.contains('header-peek')) {
+ forceHideAutoHeader();
return;
}
- const x = e.changedTouches[0].clientX;
- const leftZone = prefs.edgePadding.left + prefs.margin;
- const rightZone = prefs.edgePadding.right + prefs.margin;
- if (leftZone > 0 && x < leftZone) { goPrev(); return; }
- if (rightZone > 0 && x > window.innerWidth - rightZone) { goNext(); return; }
- if (prefs.autoHideHeader) readerLayout.classList.toggle('header-peek');
return;
}
if (absDx > SWIPE_THRESHOLD && absDy < SWIPE_MAX_VERT) {
@@ -4243,6 +4536,9 @@ epubViewer.addEventListener('touchend', handleTouchEnd, { passive: false });
function attachIframeTouchNav(view) {
const win = view?.contents?.window;
if (!win) return;
+ if (win.__codexaTouchNav) return;
+ win.__codexaTouchNav = true;
+ const touchTextMode = isTouchReader();
// No tap-to-page navigation on mobile; swipe-only navigation.
let iframeOffX = 0, iframeOffY = 0;
@@ -4255,6 +4551,9 @@ function attachIframeTouchNav(view) {
}, { passive: false });
win.addEventListener('touchmove', (e) => {
+ // Never call preventDefault during text selection — it kills Android drag handles.
+ const sel = win.getSelection?.();
+ if (touchTextMode || _selectSuppressNav || (sel && !sel.isCollapsed)) return;
const dx = Math.abs(e.touches[0].clientX + iframeOffX - touchStartX);
const dy = Math.abs(e.touches[0].clientY + iframeOffY - touchStartY);
if (dx > 8 && dx > dy) e.preventDefault();
@@ -4263,7 +4562,7 @@ function attachIframeTouchNav(view) {
win.addEventListener('touchend', (e) => {
if (suppressNextTap) {
suppressNextTap = false;
- if (e.cancelable) e.preventDefault();
+ // Do not preventDefault — that cancels Android selection handles / toolbar.
return;
}
const cx = e.changedTouches[0].clientX + iframeOffX;
@@ -4274,33 +4573,34 @@ function attachIframeTouchNav(view) {
const absDy = Math.abs(dy);
if (prefs.autoHideHeader && dy > SWIPE_DOWN_OPEN && absDx < 70) {
if (e.cancelable) e.preventDefault();
+ const wasHidden = !readerLayout.classList.contains('header-peek');
if (!readerLayout.classList.toggle('header-peek')) closeJumpPanel();
+ else if (wasHidden) _headerRevealTs = Date.now();
+ syncHeaderDismissBackdrop();
return;
}
if (prefs.autoHideHeader && dy < -SWIPE_UP_CLOSE && absDx < 70 && readerLayout.classList.contains('header-peek')) {
if (e.cancelable) e.preventDefault();
- readerLayout.classList.remove('header-peek');
+ forceHideAutoHeader();
closeJumpPanel();
return;
}
if (absDx < TAP_MAX_DRIFT && absDy < TAP_MAX_DRIFT) {
- // Tap in the margin (between text and screen edge) → navigate
- const leftZone = prefs.edgePadding.left + prefs.margin;
- const rightZone = prefs.edgePadding.right + prefs.margin;
- if (leftZone > 0 && cx < leftZone) {
+ if (prefs.autoHideHeader && inHeaderRevealZone(cy)) {
if (e.cancelable) e.preventDefault();
- goPrev();
+ revealHeader();
return;
}
- if (rightZone > 0 && cx > window.innerWidth - rightZone) {
+ const nav = inNavZone(cx);
+ if (nav) {
if (e.cancelable) e.preventDefault();
- goNext();
+ if (nav === 'prev') goPrev(); else goNext();
+ if (prefs.autoHideHeader && readerLayout.classList.contains('header-peek')) forceHideAutoHeader();
return;
}
- // Tap outside margin — reveal/toggle header
- if (prefs.autoHideHeader && cy < TOP_REVEAL_ZONE + 20) {
+ if (prefs.autoHideHeader && readerLayout.classList.contains('header-peek')) {
if (e.cancelable) e.preventDefault();
- readerLayout.classList.add('header-peek');
+ forceHideAutoHeader();
}
return;
}
@@ -4447,6 +4747,7 @@ async function resizeRenditionToViewer() {
}
window.addEventListener('resize', debounce(() => {
+ applyHeaderButtonSize();
// When running inside the Android app, system bars are always hidden in reader.
// Update layout vars here since this resize fires right after bars hide/show.
if (isAndroidApp()) {
@@ -4772,6 +5073,7 @@ document.getElementById('annot-btn-dict')?.addEventListener('click', () => {
if (word) showDictPopup(word);
});
+
// Annotations sidebar
document.getElementById('btn-annotations')?.addEventListener('click', () =>
document.getElementById('annotations-sidebar')?.classList.contains('open') ? closePanels() : openAnnotations());
@@ -4893,7 +5195,7 @@ document.getElementById('btn-search-back').addEventListener('click', async () =>
preSearchCfi = null;
searchBackBtn.style.display = 'none';
searchAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
await rendition.display(cfi);
});
document.getElementById('btn-search-accept').addEventListener('click', () => {
@@ -4901,7 +5203,7 @@ document.getElementById('btn-search-accept').addEventListener('click', () => {
preSearchCfi = null;
searchBackBtn.style.display = 'none';
searchAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
});
// Bookmark navigation back/accept — same pattern as search
document.getElementById('btn-bookmark-back').addEventListener('click', async () => {
@@ -4910,14 +5212,14 @@ document.getElementById('btn-bookmark-back').addEventListener('click', async ()
preBookmarkCfi = null;
bookmarkBackBtn.style.display = 'none';
bookmarkAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
await rendition.display(cfi);
});
document.getElementById('btn-bookmark-accept').addEventListener('click', () => {
preBookmarkCfi = null;
bookmarkBackBtn.style.display = 'none';
bookmarkAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
// Now that the user accepted the position, push progress normally
void saveProgress({ forceRemote: true });
});
@@ -4927,42 +5229,54 @@ document.getElementById('btn-annotation-back').addEventListener('click', async (
preAnnotationCfi = null;
annotationBackBtn.style.display = 'none';
annotationAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
await rendition.display(cfi);
});
document.getElementById('btn-annotation-accept').addEventListener('click', () => {
preAnnotationCfi = null;
annotationBackBtn.style.display = 'none';
annotationAcceptBtn.style.display = 'none';
- if (prefs.autoHideHeader) readerLayout.classList.remove('header-peek');
+ if (prefs.autoHideHeader) forceHideAutoHeader();
void saveProgress({ forceRemote: true });
});
document.getElementById('btn-jump-pct').addEventListener('click', () => {
- if (jumpPctPanel.style.display !== 'none') {
- closeJumpPanel();
- return;
- }
- jumpPctSlider.value = String(Math.round(currentPct * 100));
- jumpPctValue.textContent = `${jumpPctSlider.value}%`;
- jumpPctPanel.style.display = '';
+ if (isJumpPanelOpen()) closeJumpPanel();
+ else openJumpPanel();
});
-jumpPctSlider.addEventListener('input', () => {
- jumpPctValue.textContent = `${jumpPctSlider.value}%`;
-});
-jumpPctSlider.addEventListener('change', async () => {
- await seekToPercentage(parseInt(jumpPctSlider.value, 10) / 100);
-});
-document.addEventListener('mousedown', e => {
- if (jumpPctPanel.style.display === 'none') return;
- // Ignore clicks on btn-jump-pct (or any descendant — pointer-events:none on img
- // means the button is always the target, but use closest() as extra safety).
- if (e.target.closest?.('#btn-jump-pct')) return;
- // Ignore epub.js internal focus/mousedown events targeting the viewer.
- if (e.target.closest?.('#epub-viewer') || e.target === epubViewer) return;
- if (!jumpPctPanel.contains(e.target)) {
- closeJumpPanel();
+
+function syncJumpPctInputFromSlider() {
+ if (document.activeElement === jumpPctValue) return;
+ jumpPctValue.value = String(Math.round(parseInt(jumpPctSlider.value, 10) || 0));
+}
+
+function parseJumpPctInput() {
+ const raw = String(jumpPctValue?.value || '').replace(/%/g, '').trim();
+ return Math.min(100, Math.max(0, parseInt(raw, 10) || 0));
+}
+
+async function commitJumpPctInput({ seek = true } = {}) {
+ const n = parseJumpPctInput();
+ jumpPctSlider.value = String(n);
+ jumpPctValue.value = String(n);
+ if (seek) await seekToPercentage(n / 100);
+}
+
+jumpPctSlider.addEventListener('input', syncJumpPctInputFromSlider);
+jumpPctSlider.addEventListener('change', () => { void commitJumpPctInput(); });
+jumpPctValue?.addEventListener('focus', () => jumpPctValue.select());
+jumpPctValue?.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ void commitJumpPctInput().then(() => jumpPctValue.blur());
+ } else if (e.key === 'Escape') {
+ syncJumpPctInputFromSlider();
+ jumpPctValue.blur();
}
});
+jumpPctValue?.addEventListener('blur', () => { void commitJumpPctInput(); });
+
+jumpPctBackdrop?.addEventListener('click', closeJumpPanel);
+jumpPctBackdrop?.addEventListener('touchend', (e) => { e.preventDefault(); closeJumpPanel(); }, { passive: false });
document.getElementById('search-close').addEventListener('click', closePanels);
document.getElementById('search-submit').addEventListener('click', () => {
const q = searchInput.value.trim();
@@ -4987,52 +5301,47 @@ onTap(panelBackdrop, closePanels);
document.getElementById('btn-prev').addEventListener('click', e => { e.stopPropagation(); goPrev(); });
document.getElementById('btn-next').addEventListener('click', e => { e.stopPropagation(); goNext(); });
-// ── Chapter navigation buttons on book progress bar ────────────────────────────
-function findCurrentTocChapIdx() {
- const norm = h => (h || '').split('#')[0].replace(/_split_\d+(\.\w+)$/, '$1').toLowerCase();
- const base = norm(currentHref).split('/').pop();
- const tops = tocFlatItems.filter(t => t.depth === 0);
- let found = -1;
- tops.forEach((t, i) => {
- const tb = norm(t.href || '').split('/').pop();
- if (base && tb && (base === tb || base.includes(tb) || tb.includes(base))) found = i;
+// ── Chapter navigation buttons on jump-to-% panel ─────────────────────────────
+function bindJumpChapNav(btnId, direction) {
+ const btn = document.getElementById(btnId);
+ if (!btn) return;
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const { tops, idx } = findCurrentTopLevelChapIdx();
+ if (!tops.length) return;
+ let target = null;
+ if (direction === 'prev') {
+ if (idx > 0) target = tops[idx - 1];
+ else if (idx === 0) target = tops[0];
+ else target = tops[0];
+ } else if (idx < 0) {
+ const curSpine = rendition?.currentLocation?.()?.start?.index;
+ target = curSpine != null
+ ? tops.find(t => {
+ const si = findSpineItemForHref((t.href || '').split('#')[0]);
+ return si?.index != null && si.index > curSpine;
+ }) || null
+ : tops[0];
+ } else if (idx < tops.length - 1) {
+ target = tops[idx + 1];
+ }
+ if (!target) return;
+ closeJumpPanel();
+ await navigateToTocChapter(target);
});
- return { tops, idx: found };
-}
-// Close the jump panel automatically after chapter nav button navigation.
-// This matches TOC behaviour and avoids the panel being stuck open with
-// the header invisible (autohide removes header-peek on close but epub.js
-// sometimes re-focuses the iframe making btn-jump-pct unreachable).
-document.getElementById('btn-prev-chap')?.addEventListener('click', () => {
- const { tops, idx } = findCurrentTocChapIdx();
- const target = idx > 0 ? tops[idx - 1] : (idx === 0 ? tops[0] : null);
- if (!target) return;
- const spineItem = findSpineItemForHref((target.href || '').split('#')[0]);
- const displayTarget = spineItem?.index != null ? spineItem.index : target.href;
- console.log(`[nav] PREV-CHAP | from="${currentHref?.split('/').pop()}" → "${displayTarget}"`);
- rendition?.display(displayTarget).then(() => {
- const loc = rendition?.currentLocation?.();
- console.log(`[nav] CHAP-landed | page=${loc?.start?.displayed?.page}/${loc?.start?.displayed?.total} href="${loc?.start?.href?.split('/').pop()}"`);
- scheduleBionicPrefetchAround(loc);
- }).catch(() => {});
-});
-document.getElementById('btn-next-chap')?.addEventListener('click', () => {
- const { tops, idx } = findCurrentTocChapIdx();
- if (idx < 0 || idx >= tops.length - 1) return;
- const target = tops[idx + 1];
- const spineItem = findSpineItemForHref((target.href || '').split('#')[0]);
- const displayTarget = spineItem?.index != null ? spineItem.index : target.href;
- console.log(`[nav] NEXT-CHAP | from="${currentHref?.split('/').pop()}" → "${displayTarget}"`);
- rendition?.display(displayTarget).then(() => {
- const loc = rendition?.currentLocation?.();
- console.log(`[nav] CHAP-landed | page=${loc?.start?.displayed?.page}/${loc?.start?.displayed?.total} href="${loc?.start?.href?.split('/').pop()}"`);
- scheduleBionicPrefetchAround(loc);
- }).catch(() => {});
-});
-document.querySelector('.nav-zone-prev')?.addEventListener('click', goPrev);
-document.querySelector('.nav-zone-next')?.addEventListener('click', goNext);
-document.querySelector('.nav-zone-prev')?.addEventListener('touchend', (e) => { if (e.cancelable) e.preventDefault(); goPrev(); }, { passive: false });
-document.querySelector('.nav-zone-next')?.addEventListener('touchend', (e) => { if (e.cancelable) e.preventDefault(); goNext(); }, { passive: false });
+}
+bindJumpChapNav('btn-prev-chap', 'prev');
+bindJumpChapNav('btn-next-chap', 'next');
+function handleNavZoneActivate(direction, e) {
+ const y = e.clientY ?? e.changedTouches?.[0]?.clientY;
+ if (prefs.autoHideHeader && y != null && inHeaderRevealZone(y)) {
+ revealHeader();
+ return;
+ }
+ if (direction === 'prev') goPrev(); else goNext();
+}
+document.querySelector('.nav-zone-prev')?.addEventListener('click', e => { e.stopPropagation(); handleNavZoneActivate('prev', e); });
+document.querySelector('.nav-zone-next')?.addEventListener('click', e => { e.stopPropagation(); handleNavZoneActivate('next', e); });
document.getElementById('btn-prev').addEventListener('keydown', e => { if (e.key === 'Enter') goPrev(); });
document.getElementById('btn-next').addEventListener('keydown', e => { if (e.key === 'Enter') goNext(); });
window.addEventListener('beforeunload', () => {
@@ -5102,6 +5411,9 @@ async function init() {
document.title = `${currentBook.title} — Codexa`;
loadBookPrefs(currentBook.id);
syncSettingsUi();
+ if (currentBook.id) {
+ apiFetch(`/books/${currentBook.id}/opened`, { method: 'POST' }).catch(() => {});
+ }
} catch {
const msg = t('reader.err_no_book');
loadingMsg.textContent = msg;
@@ -5119,13 +5431,16 @@ async function init() {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
arrayBuffer = await res.arrayBuffer();
} catch {
- const msg = !navigator.onLine
- ? t('reader.err_offline_not_cached')
- : t('reader.err_download');
- loadingMsg.textContent = msg;
- toast.error(msg);
- setTimeout(() => { window.location.href = '/'; }, 2500);
- return;
+ arrayBuffer = await fetchOfflineBookFile(bookId);
+ if (!arrayBuffer) {
+ const msg = !navigator.onLine
+ ? t('reader.err_offline_not_cached')
+ : t('reader.err_download');
+ loadingMsg.textContent = msg;
+ toast.error(msg);
+ setTimeout(() => { window.location.href = '/'; }, 2500);
+ return;
+ }
}
// Auto-download to offline cache in background after successful file load (skip in peek mode)
@@ -5197,11 +5512,9 @@ async function init() {
if (localProgress?.percentage > 0) {
lastKnownGoodPct = localProgress.percentage;
// Keep the offline metadata in sync so "Currently Reading" is correct offline
- if (!isPeekMode) {
- getBookMeta(Number(bookId)).then(meta => {
- if (meta) saveBookMeta({ ...meta, percentage: localProgress.percentage }).catch(() => {});
- }).catch(() => {});
- }
+ getBookMeta(Number(bookId)).then(meta => {
+ if (meta) saveBookMeta({ ...meta, percentage: localProgress.percentage }).catch(() => {});
+ }).catch(() => {});
}
if (!startCfi && localProgress?.cfi_position) startCfi = localProgress.cfi_position;
// Pre-load locations from cache so seekToPercentage works immediately after startRendition
@@ -5348,7 +5661,8 @@ async function init() {
// Only allow saves after the initial position (local or synced) is fully displayed
console.log('[pos] isReady=true, currentCfi:', currentCfi.slice(0,60));
isReady = true;
- openCfi = currentCfi; // snapshot position-on-open for change detection
+ openCfi = currentCfi; // snapshot position-on-open for change detection
+ void initBattery();
// Load bookmarks and annotations for this book (non-blocking)
void loadBookmarks(currentBook.id);
void loadAnnotations(currentBook.id);
@@ -5373,6 +5687,7 @@ init();
// Re-render settings panel content when language changes
document.addEventListener('langchange', () => {
applyTranslations();
+ syncSettingsUi();
syncFullscreenButton();
renderSbItems();
renderDictSettings();
diff --git a/public/js/sw-register.js b/public/js/sw-register.js
new file mode 100644
index 0000000..ae40630
--- /dev/null
+++ b/public/js/sw-register.js
@@ -0,0 +1,26 @@
+// Service worker registration — network-first updates, reload when a new version activates.
+(function () {
+ if (!('serviceWorker' in navigator)) return;
+
+ let reloading = false;
+
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
+ if (reloading) return;
+ reloading = true;
+ location.reload();
+ });
+
+ function checkForUpdates(reg) {
+ try { reg.update(); } catch { /* ignore */ }
+ }
+
+ navigator.serviceWorker.register('/sw.js')
+ .then((reg) => {
+ checkForUpdates(reg);
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') checkForUpdates(reg);
+ });
+ window.addEventListener('focus', () => checkForUpdates(reg));
+ })
+ .catch(() => {});
+})();
diff --git a/public/locales/de.json b/public/locales/de.json
index d300e20..a6dee1a 100644
--- a/public/locales/de.json
+++ b/public/locales/de.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Titel Z–A",
"library.sort_author_az": "Autor A–Z",
"library.sort_progress": "Fortschritt",
+ "library.sort_last_opened": "Zuletzt geöffnet",
"library.sort_series": "Reihe",
"library.btn_edit": "Bearbeiten",
"library.add_book": "Buch hinzufügen",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Löschen",
"library.btn_download": "Herunterladen",
"library.btn_read": "Lesen",
- "library.btn_peek": "Vorschau",
"library.btn_cover_preview": "Cover-Vorschau",
"library.btn_cover_info": "Buchdetails",
"library.btn_cover_delete": "Buch löschen",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "Falls aktiviert, öffnet das Buch immer am Anfang ohne Prüfung der synchronisierten Position",
"reader.sb_skip_close": "Beim Schließen nicht speichern",
"reader.sb_skip_close_hint": "Falls aktiviert, wird die Position beim Verlassen nicht automatisch gespeichert",
+ "reader.sb_save_on_hide": "Bei Bildschirmsperre / App-Wechsel speichern",
+ "reader.sb_save_on_hide_hint": "Lesezeiger senden, wenn der Bildschirm gesperrt wird oder Sie die App wechseln",
"reader.sb_hyphenation": "Silbentrennung",
"reader.sb_hyphenation_hint": "Automatische Worttrennung an Zeilenumbrüchen",
"reader.sb_bionic": "Bionisches Lesen",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Automatisch (vom Buch)",
"reader.sb_page_shadow": "Seitenrand (Buchfalz)",
"reader.sb_page_shadow_hint": "Dunkler Rand zwischen den Seiten im zweispaltigen Modus",
+ "reader.sb_section_navigation": "Seitennavigation",
+ "reader.sb_nav_zone_left": "Zurück-Zone (linker Rand)",
+ "reader.sb_nav_zone_right": "Vorwärts-Zone (rechter Rand)",
+ "reader.sb_nav_zone_hint": "Tippen oder klicken Sie auf diesen Bildschirmbereich, um Seiten umzublättern.",
+ "reader.sb_section_toolbar": "Symbolleiste",
+ "reader.sb_header_reveal_zone": "Symbolleisten-Anzeigebereich",
+ "reader.sb_header_reveal_hint": "Prozentsatz der Bildschirmhöhe vom oberen Rand, bei dem ein Tipp die Symbolleiste einblendet",
+ "reader.sb_header_button_size": "Größe der Symbolleisten-Schaltflächen",
+ "reader.sb_header_button_hint": "Prozentsatz der Standardgröße (100 % = normal)",
"reader.sb_section_edge_pad": "Bildschirmrand-Abstand",
"reader.sb_edge_pad_hint": "Für gewölbte Displays — verschiebt Inhalt und Statusleiste vom Rand weg.",
"reader.sb_section_statusbar": "Statusleiste",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Dicke",
"reader.sb_book_prog": "Buchfortschritt (Balken)",
"reader.sb_chap_prog": "Kapitelfortschritt (Balken)",
+ "reader.sb_clock_format": "Uhrzeitformat",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Wörterbücher",
"reader.sb_dicts_hint": "Wörterbücher auswählen und Suchreihenfolge festlegen (↑↓)",
"reader.dict_no_dicts": "Keine Wörterbücher. Füge .ifo/.idx/.dict Dateien dem Ordner data/dictionaries/ hinzu.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Vollbild beenden",
"reader.err_no_fullscreen": "Vollbildmodus ist nicht verfügbar.",
"reader.sync_dlg_title": "KOReader-Sync",
+ "reader.sync_dlg_hint": "Das Schließen dieses Dialogs springt zur fortgeschritteneren Position, um Ihren Fortschritt zu schützen.",
"reader.sync_dlg_col_device": "Gerät",
"reader.sync_dlg_col_pos": "Position",
"reader.sync_dlg_col_time": "Zeit",
@@ -362,8 +377,6 @@
"opds.sync_title": "In Regal synchronisieren",
"opds.err_browse": "Fehler beim Durchsuchen: {msg}",
"opds.btn_add": "+ Zur Bibliothek hinzufügen",
- "opds.btn_read": "Lesen",
- "opds.btn_peek": "Vorschau",
"opds.no_description": "Keine Beschreibung vorhanden.",
"opds.btn_downloading": "Herunterladen…",
"opds.toast_book_added": "\"{title}\" zur Bibliothek hinzugefügt.",
@@ -394,10 +407,11 @@
"opds.page_next": "Nächste",
"opds.page_info": "Seite {page} · {count} Bücher",
"reader.dict_loading": "Suche…",
+ "reader.dict_lookup_chip": "Nachschlagen",
"error.session_expired": "Sitzung abgelaufen. Bitte erneut anmelden.",
"error.http_error": "Fehler {status}",
"error.credentials_required": "Benutzername und Passwort sind erforderlich.",
- "error.username_invalid": "Benutzername muss 3\u201332 Zeichen haben.",
+ "error.username_invalid": "Benutzername muss 3–32 Zeichen haben.",
"error.password_too_short": "Das Passwort muss mindestens 8 Zeichen lang sein.",
"error.registration_disabled": "Die Registrierung neuer Benutzer ist deaktiviert.",
"error.username_taken": "Der Benutzername ist bereits vergeben.",
@@ -496,5 +510,6 @@
"reader.no_custom_fonts": "Keine eigenen Schriften",
"reader.upload_fonts": "Schriften hochladen…",
"reader.upload_dict": "Wörterbuch ZIP hochladen…",
- "reader.uploading": "Hochladen"
+ "reader.uploading": "Hochladen",
+ "reader.sb_battery": "Akku"
}
diff --git a/public/locales/en.json b/public/locales/en.json
index 0077914..92f621b 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Title Z–A",
"library.sort_author_az": "Author A–Z",
"library.sort_progress": "Progress",
+ "library.sort_last_opened": "Last opened",
"library.sort_series": "Series",
"library.btn_edit": "Edit",
"library.add_book": "Add book",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Delete",
"library.btn_download": "Download",
"library.btn_read": "Read",
- "library.btn_peek": "Peek",
"library.btn_cover_preview": "Cover preview",
"library.btn_cover_info": "Book details",
"library.btn_cover_delete": "Delete book",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "If enabled, book always opens from the beginning without checking saved/synced position",
"reader.sb_skip_close": "Do not save on close",
"reader.sb_skip_close_hint": "If enabled, position is not saved automatically when returning to library or closing the tab",
+ "reader.sb_save_on_hide": "Save on tablet lock / app switch",
+ "reader.sb_save_on_hide_hint": "Push reading position to the server when the screen locks or you switch away.",
"reader.sb_hyphenation": "Word hyphenation",
"reader.sb_hyphenation_hint": "Automatically hyphenate words at line breaks",
"reader.sb_bionic": "Bionic reading",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Automatic (from book)",
"reader.sb_page_shadow": "Page border (book spine)",
"reader.sb_page_shadow_hint": "Dark border between pages in two-page mode",
+ "reader.sb_section_navigation": "Page navigation",
+ "reader.sb_nav_zone_left": "Back zone (left edge)",
+ "reader.sb_nav_zone_right": "Forward zone (right edge)",
+ "reader.sb_nav_zone_hint": "Tap or click this portion of the screen to turn pages.",
+ "reader.sb_section_toolbar": "Toolbar",
+ "reader.sb_header_reveal_zone": "Toolbar reveal zone",
+ "reader.sb_header_reveal_hint": "Percentage of screen height from the top where tap reveals the toolbar",
+ "reader.sb_header_button_size": "Toolbar button size",
+ "reader.sb_header_button_hint": "Percentage of the default button size (100% = normal)",
"reader.sb_section_edge_pad": "Screen edge padding",
"reader.sb_edge_pad_hint": "For curved phone screens — moves content and status bar away from the edge.",
"reader.sb_section_statusbar": "Status bar",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Thickness",
"reader.sb_book_prog": "Book progress (bar)",
"reader.sb_chap_prog": "Chapter progress (bar)",
+ "reader.sb_clock_format": "Clock format",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Dictionaries",
"reader.sb_dicts_hint": "Select dictionaries and set search order (↑↓)",
"reader.dict_no_dicts": "No dictionaries. Add .ifo/.idx/.dict files to the data/dictionaries/ folder.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Exit fullscreen",
"reader.err_no_fullscreen": "Fullscreen is not available.",
"reader.sync_dlg_title": "KOReader Sync",
+ "reader.sync_dlg_hint": "Dismissing this dialog will jump to the further position to protect your progress.",
"reader.sync_dlg_col_device": "Device",
"reader.sync_dlg_col_pos": "Position",
"reader.sync_dlg_col_time": "Time",
@@ -362,8 +377,6 @@
"opds.sync_title": "Sync to shelf",
"opds.err_browse": "Browse error: {msg}",
"opds.btn_add": "+ Add to library",
- "opds.btn_read": "Read",
- "opds.btn_peek": "Peek",
"opds.no_description": "No description.",
"opds.btn_downloading": "Downloading…",
"opds.toast_book_added": "\"{title}\" added to library.",
@@ -394,10 +407,11 @@
"opds.page_next": "Next",
"opds.page_info": "Page {page} · {count} books",
"reader.dict_loading": "Searching…",
+ "reader.dict_lookup_chip": "Lookup",
"error.session_expired": "Session expired. Please sign in again.",
"error.http_error": "Error {status}",
"error.credentials_required": "Username and password are required.",
- "error.username_invalid": "Username must be 3\u201332 characters (letters, numbers, underscore).",
+ "error.username_invalid": "Username must be 3–32 characters (letters, numbers, underscore).",
"error.password_too_short": "Password must be at least 8 characters.",
"error.registration_disabled": "Registration of new users is disabled.",
"error.username_taken": "Username is already taken.",
@@ -451,6 +465,7 @@
"reader.prev_chapter": "Previous chapter",
"reader.next_chapter": "Next chapter",
"reader.btn_jump_pct": "Jump to position",
+ "reader.jump_pct_input": "Jump to percentage",
"reader.btn_search": "Search in book",
"reader.btn_search_accept": "Accept position",
"reader.btn_search_back": "Back to previous position",
@@ -492,9 +507,14 @@
"reader.annotation_color_blue": "Blue",
"reader.annotation_color_pink": "Pink",
"reader.annotation_dict_lookup": "Dictionary",
+ "reader.annotation_copy": "Copy",
+ "reader.annotation_share": "Share",
+ "reader.annotation_copied": "Copied to clipboard",
+ "reader.annotation_copy_failed": "Could not copy text",
"reader.custom_fonts": "Custom fonts",
"reader.no_custom_fonts": "No custom fonts",
"reader.upload_fonts": "Upload fonts…",
"reader.upload_dict": "Upload dictionary ZIP…",
- "reader.uploading": "Uploading"
-}
+ "reader.uploading": "Uploading",
+ "reader.sb_battery": "Battery"
+}
\ No newline at end of file
diff --git a/public/locales/es.json b/public/locales/es.json
index f09229f..af879b6 100644
--- a/public/locales/es.json
+++ b/public/locales/es.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Título Z–A",
"library.sort_author_az": "Autor A–Z",
"library.sort_progress": "Progreso",
+ "library.sort_last_opened": "Última apertura",
"library.sort_series": "Serie",
"library.btn_edit": "Editar",
"library.add_book": "Añadir libro",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Eliminar",
"library.btn_download": "Descargar",
"library.btn_read": "Leer",
- "library.btn_peek": "Vista previa",
"library.btn_cover_preview": "Vista previa de portada",
"library.btn_cover_info": "Detalles del libro",
"library.btn_cover_delete": "Eliminar libro",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "Si se activa, el libro siempre abre al inicio sin comprobar la posición guardada",
"reader.sb_skip_close": "No guardar al cerrar",
"reader.sb_skip_close_hint": "Si se activa, la posición no se guarda al volver a la biblioteca o cerrar la pestaña",
+ "reader.sb_save_on_hide": "Guardar al bloquear pantalla / cambiar de app",
+ "reader.sb_save_on_hide_hint": "Envía la posición al servidor al bloquear la pantalla o cambiar de aplicación",
"reader.sb_hyphenation": "División de palabras",
"reader.sb_hyphenation_hint": "Dividir palabras automáticamente con guiones al final de línea",
"reader.sb_bionic": "Lectura biónica",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Automático (del libro)",
"reader.sb_page_shadow": "Borde de página (lomo)",
"reader.sb_page_shadow_hint": "Borde oscuro entre páginas en modo de dos páginas",
+ "reader.sb_section_navigation": "Navegación de páginas",
+ "reader.sb_nav_zone_left": "Zona atrás (borde izquierdo)",
+ "reader.sb_nav_zone_right": "Zona adelante (borde derecho)",
+ "reader.sb_nav_zone_hint": "Toque o haga clic en esta parte de la pantalla para pasar páginas.",
+ "reader.sb_section_toolbar": "Barra de herramientas",
+ "reader.sb_header_reveal_zone": "Zona de revelación de la barra",
+ "reader.sb_header_reveal_hint": "Porcentaje de la altura de pantalla desde arriba donde un toque muestra la barra",
+ "reader.sb_header_button_size": "Tamaño de los botones de la barra",
+ "reader.sb_header_button_hint": "Porcentaje del tamaño predeterminado (100 % = normal)",
"reader.sb_section_edge_pad": "Margen del borde de pantalla",
"reader.sb_edge_pad_hint": "Para pantallas curvas: aleja el contenido y la barra de estado del borde.",
"reader.sb_section_statusbar": "Barra de estado",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Grosor",
"reader.sb_book_prog": "Progreso del libro (barra)",
"reader.sb_chap_prog": "Progreso del capítulo (barra)",
+ "reader.sb_clock_format": "Formato de hora",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Diccionarios",
"reader.sb_dicts_hint": "Elige diccionarios y orden de búsqueda (↑↓)",
"reader.dict_no_dicts": "Sin diccionarios. Añade archivos .ifo/.idx/.dict a la carpeta data/dictionaries/.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Salir de pantalla completa",
"reader.err_no_fullscreen": "Pantalla completa no disponible.",
"reader.sync_dlg_title": "Sincronización KOReader",
+ "reader.sync_dlg_hint": "Cerrar este diálogo saltará a la posición más avanzada para proteger tu progreso.",
"reader.sync_dlg_col_device": "Dispositivo",
"reader.sync_dlg_col_pos": "Posición",
"reader.sync_dlg_col_time": "Hora",
@@ -362,8 +377,6 @@
"opds.sync_title": "Sincronizar a estantería",
"opds.err_browse": "Error de navegación: {msg}",
"opds.btn_add": "+ Añadir a biblioteca",
- "opds.btn_read": "Leer",
- "opds.btn_peek": "Vista previa",
"opds.no_description": "Sin descripción.",
"opds.btn_downloading": "Descargando…",
"opds.toast_book_added": "\"{title}\" añadido a la biblioteca.",
@@ -394,10 +407,11 @@
"opds.page_next": "Siguiente",
"opds.page_info": "Página {page} · {count} libros",
"reader.dict_loading": "Buscando…",
+ "reader.dict_lookup_chip": "Buscar",
"error.session_expired": "Sesión expirada. Inicie sesión de nuevo.",
"error.http_error": "Error {status}",
"error.credentials_required": "Se requieren nombre de usuario y contraseña.",
- "error.username_invalid": "El nombre de usuario debe tener 3\u201332 caracteres.",
+ "error.username_invalid": "El nombre de usuario debe tener 3–32 caracteres.",
"error.password_too_short": "La contraseña debe tener al menos 8 caracteres.",
"error.registration_disabled": "El registro de nuevos usuarios está desactivado.",
"error.username_taken": "El nombre de usuario ya está en uso.",
@@ -496,5 +510,6 @@
"reader.no_custom_fonts": "Sin fuentes propias",
"reader.upload_fonts": "Subir fuentes…",
"reader.upload_dict": "Subir diccionario ZIP…",
- "reader.uploading": "Subiendo"
+ "reader.uploading": "Subiendo",
+ "reader.sb_battery": "Batería"
}
diff --git a/public/locales/fr.json b/public/locales/fr.json
index 63a0a36..60fffa9 100644
--- a/public/locales/fr.json
+++ b/public/locales/fr.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Titre Z–A",
"library.sort_author_az": "Auteur A–Z",
"library.sort_progress": "Progression",
+ "library.sort_last_opened": "Dernière ouverture",
"library.sort_series": "Série",
"library.btn_edit": "Modifier",
"library.add_book": "Ajouter un livre",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Supprimer",
"library.btn_download": "Télécharger",
"library.btn_read": "Lire",
- "library.btn_peek": "Aperçu",
"library.btn_cover_preview": "Aperçu de la couverture",
"library.btn_cover_info": "Détails du livre",
"library.btn_cover_delete": "Supprimer le livre",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "Si activé, le livre s'ouvrira toujours au début sans vérifier la position synchronisée",
"reader.sb_skip_close": "Ne pas enregistrer à la fermeture",
"reader.sb_skip_close_hint": "Si activé, la position ne sera pas enregistrée automatiquement en quittant",
+ "reader.sb_save_on_hide": "Enregistrer au verrouillage / changement d'app",
+ "reader.sb_save_on_hide_hint": "Envoie la position au serveur quand l'écran se verrouille ou que vous changez d'application",
"reader.sb_hyphenation": "Césure",
"reader.sb_hyphenation_hint": "Coupure automatique des mots en fin de ligne",
"reader.sb_bionic": "Lecture bionique",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Automatique (depuis le livre)",
"reader.sb_page_shadow": "Ombre de page (pliure)",
"reader.sb_page_shadow_hint": "Bordure sombre entre les pages en mode deux colonnes",
+ "reader.sb_section_navigation": "Navigation des pages",
+ "reader.sb_nav_zone_left": "Zone précédent (bord gauche)",
+ "reader.sb_nav_zone_right": "Zone suivant (bord droit)",
+ "reader.sb_nav_zone_hint": "Appuyez ou cliquez sur cette portion de l'écran pour tourner les pages.",
+ "reader.sb_section_toolbar": "Barre d'outils",
+ "reader.sb_header_reveal_zone": "Zone d'affichage de la barre",
+ "reader.sb_header_reveal_hint": "Pourcentage de la hauteur d'écran depuis le haut où un appui affiche la barre d'outils",
+ "reader.sb_header_button_size": "Taille des boutons de la barre",
+ "reader.sb_header_button_hint": "Pourcentage de la taille par défaut (100 % = normal)",
"reader.sb_section_edge_pad": "Retrait des bords de l'écran",
"reader.sb_edge_pad_hint": "Pour les écrans incurvés — éloigne le contenu et la barre d'état des bords.",
"reader.sb_section_statusbar": "Barre d'état",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Épaisseur",
"reader.sb_book_prog": "Progression du livre (barre)",
"reader.sb_chap_prog": "Progression du chapitre (barre)",
+ "reader.sb_clock_format": "Format de l'heure",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Dictionnaires",
"reader.sb_dicts_hint": "Choisir les dictionnaires et l'ordre de recherche (↑↓)",
"reader.dict_no_dicts": "Aucun dictionnaire. Ajoutez des fichiers .ifo/.idx/.dict dans data/dictionaries/.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Quitter le plein écran",
"reader.err_no_fullscreen": "Le mode plein écran n'est pas disponible.",
"reader.sync_dlg_title": "Synchronisation KOReader",
+ "reader.sync_dlg_hint": "Fermer cette fenêtre sautera à la position la plus avancée pour protéger votre progression.",
"reader.sync_dlg_col_device": "Appareil",
"reader.sync_dlg_col_pos": "Position",
"reader.sync_dlg_col_time": "Heure",
@@ -362,8 +377,6 @@
"opds.sync_title": "Synchroniser dans une étagère",
"opds.err_browse": "Erreur de navigation : {msg}",
"opds.btn_add": "+ Ajouter à la bibliothèque",
- "opds.btn_read": "Lire",
- "opds.btn_peek": "Aperçu",
"opds.no_description": "Aucune description disponible.",
"opds.btn_downloading": "Téléchargement…",
"opds.toast_book_added": "\"{title}\" ajouté à la bibliothèque.",
@@ -394,10 +407,11 @@
"opds.page_next": "Suivante",
"opds.page_info": "Page {page} · {count} livres",
"reader.dict_loading": "Recherche…",
+ "reader.dict_lookup_chip": "Chercher",
"error.session_expired": "Session expirée. Veuillez vous reconnecter.",
"error.http_error": "Erreur {status}",
"error.credentials_required": "Le nom d'utilisateur et le mot de passe sont obligatoires.",
- "error.username_invalid": "Le nom d'utilisateur doit comporter 3\u201332 caractères.",
+ "error.username_invalid": "Le nom d'utilisateur doit comporter 3–32 caractères.",
"error.password_too_short": "Le mot de passe doit comporter au moins 8 caractères.",
"error.registration_disabled": "L'inscription de nouveaux utilisateurs est désactivée.",
"error.username_taken": "Ce nom d'utilisateur est déjà pris.",
@@ -496,5 +510,6 @@
"reader.no_custom_fonts": "Aucune police personnalisée",
"reader.upload_fonts": "Télécharger des polices…",
"reader.upload_dict": "Télécharger un dico ZIP…",
- "reader.uploading": "Téléchargement"
+ "reader.uploading": "Téléchargement",
+ "reader.sb_battery": "Batterie"
}
diff --git a/public/locales/it.json b/public/locales/it.json
index a18dc04..9ced46d 100644
--- a/public/locales/it.json
+++ b/public/locales/it.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Titolo Z–A",
"library.sort_author_az": "Autore A–Z",
"library.sort_progress": "Progresso",
+ "library.sort_last_opened": "Ultima apertura",
"library.btn_edit": "Modifica",
"library.add_book": "Aggiungi libro",
"library.upload_file": "Carica file",
@@ -91,7 +92,6 @@
"library.btn_del_book": "Elimina",
"library.btn_download": "Scarica",
"library.btn_read": "Leggi",
- "library.btn_peek": "Anteprima",
"library.btn_cover_preview": "Anteprima copertina",
"library.btn_cover_info": "Dettagli del libro",
"library.btn_cover_delete": "Elimina libro",
@@ -194,6 +194,8 @@
"reader.sb_skip_open_hint": "Se attivo, il libro si aprirà sempre dall'inizio ignorando la posizione sincronizzata",
"reader.sb_skip_close": "Non salvare alla chiusura",
"reader.sb_skip_close_hint": "Se attivo, la posizione non verrà salvata automaticamente all'uscita",
+ "reader.sb_save_on_hide": "Salva al blocco schermo / cambio app",
+ "reader.sb_save_on_hide_hint": "Invia la posizione al server quando lo schermo si blocca o cambi app",
"reader.sb_hyphenation": "Sillabazione",
"reader.sb_hyphenation_hint": "Sillabazione automatica delle parole a fine riga",
"reader.sb_bionic": "Lettura bionica",
@@ -202,6 +204,15 @@
"reader.sb_hyphenation_lang_auto": "Automatica (dal libro)",
"reader.sb_page_shadow": "Ombra pagina (rilegatura)",
"reader.sb_page_shadow_hint": "Margine scuro tra le pagine in modalità a due colonne",
+ "reader.sb_section_navigation": "Navigazione pagine",
+ "reader.sb_nav_zone_left": "Zona indietro (bordo sinistro)",
+ "reader.sb_nav_zone_right": "Zona avanti (bordo destro)",
+ "reader.sb_nav_zone_hint": "Tocca o clicca questa parte dello schermo per girare pagina.",
+ "reader.sb_section_toolbar": "Barra degli strumenti",
+ "reader.sb_header_reveal_zone": "Zona di visualizzazione barra",
+ "reader.sb_header_reveal_hint": "Percentuale dell'altezza dello schermo dall'alto dove un tocco mostra la barra",
+ "reader.sb_header_button_size": "Dimensione pulsanti barra",
+ "reader.sb_header_button_hint": "Percentuale della dimensione predefinita (100 % = normale)",
"reader.sb_section_edge_pad": "Distanza dai bordi dello schermo",
"reader.sb_edge_pad_hint": "Per schermi curvi — allontana contenuto e barra di stato dai bordi.",
"reader.sb_section_statusbar": "Barra di stato",
@@ -212,6 +223,9 @@
"reader.sb_sep_thickness": "Spessore",
"reader.sb_book_prog": "Progresso libro (barra)",
"reader.sb_chap_prog": "Progreso capitolo (barra)",
+ "reader.sb_clock_format": "Formato ora",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Dizionari",
"reader.sb_dicts_hint": "Scegli i dizionari e l'ordine di ricerca (↑↓)",
"reader.dict_no_dicts": "Nessun dizionario trovato. Aggiungi i file .ifo/.idx/.dict nella cartella data/dictionaries/.",
@@ -247,6 +261,7 @@
"reader.btn_fullscreen_exit": "Esci da schermo intero",
"reader.err_no_fullscreen": "Modalità schermo intero non disponibile.",
"reader.sync_dlg_title": "Sincronizzazione KOReader",
+ "reader.sync_dlg_hint": "Chiudere questa finestra salterà alla posizione più avanzata per proteggere i tuoi progressi.",
"reader.sync_dlg_col_device": "Dispositivo",
"reader.sync_dlg_col_pos": "Posizione",
"reader.sync_dlg_col_time": "Ora",
@@ -361,8 +376,6 @@
"opds.sync_title": "Sincronizza in scaffale",
"opds.err_browse": "Errore di navigazione: {msg}",
"opds.btn_add": "+ Aggiungi alla biblioteca",
- "opds.btn_read": "Leggi",
- "opds.btn_peek": "Anteprima",
"opds.no_description": "Nessuna descrizione.",
"opds.btn_downloading": "Download in corso…",
"opds.toast_book_added": "\"{title}\" aggiunto alla biblioteca.",
@@ -393,10 +406,11 @@
"opds.page_next": "Successiva",
"opds.page_info": "Pagina {page} · {count} libri",
"reader.dict_loading": "Ricerca…",
+ "reader.dict_lookup_chip": "Cerca",
"error.session_expired": "Sessione scaduta. Effettua di nuovo l'accesso.",
"error.http_error": "Errore {status}",
"error.credentials_required": "Nome utente e password sono obbligatori.",
- "error.username_invalid": "Il nome utente deve avere 3\u201332 caratteri.",
+ "error.username_invalid": "Il nome utente deve avere 3–32 caratteri.",
"error.password_too_short": "La password deve avere almeno 8 caratteri.",
"error.registration_disabled": "La registrazione di nuovi utenti è disabilitata.",
"error.username_taken": "Il nome utente è già in uso.",
@@ -495,5 +509,6 @@
"reader.no_custom_fonts": "Nessun font personalizzato",
"reader.upload_fonts": "Carica font…",
"reader.upload_dict": "Carica dizionario ZIP…",
- "reader.uploading": "Caricamento"
+ "reader.uploading": "Caricamento",
+ "reader.sb_battery": "Batteria"
}
diff --git a/public/locales/pt.json b/public/locales/pt.json
index 1d3bcae..756cca7 100644
--- a/public/locales/pt.json
+++ b/public/locales/pt.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Título Z–A",
"library.sort_author_az": "Autor A–Z",
"library.sort_progress": "Progresso",
+ "library.sort_last_opened": "Última abertura",
"library.sort_series": "Série",
"library.btn_edit": "Editar",
"library.add_book": "Adicionar livro",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Eliminar",
"library.btn_download": "Descarregar",
"library.btn_read": "Ler",
- "library.btn_peek": "Pré-visualizar",
"library.btn_cover_preview": "Pré-visualização da capa",
"library.btn_cover_info": "Detalhes do livro",
"library.btn_cover_delete": "Eliminar livro",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "Se ativado, abre sempre no início",
"reader.sb_skip_close": "Não guardar ao fechar",
"reader.sb_skip_close_hint": "Se ativado, a posição não será guardada ao sair",
+ "reader.sb_save_on_hide": "Guardar ao bloquear ecrã / mudar de app",
+ "reader.sb_save_on_hide_hint": "Envia a posição ao servidor quando o ecrã bloqueia ou muda de aplicação",
"reader.sb_hyphenation": "Hifenização",
"reader.sb_hyphenation_hint": "Quebra automática de palavras ao fim da linha",
"reader.sb_bionic": "Leitura biónica",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Automático (do livro)",
"reader.sb_page_shadow": "Sombra da página (vinco)",
"reader.sb_page_shadow_hint": "Margem escura entre páginas no modo de duas colunas",
+ "reader.sb_section_navigation": "Navegação de páginas",
+ "reader.sb_nav_zone_left": "Zona anterior (borda esquerda)",
+ "reader.sb_nav_zone_right": "Zona seguinte (borda direita)",
+ "reader.sb_nav_zone_hint": "Toque ou clique nesta parte do ecrã para virar páginas.",
+ "reader.sb_section_toolbar": "Barra de ferramentas",
+ "reader.sb_header_reveal_zone": "Zona de revelação da barra",
+ "reader.sb_header_reveal_hint": "Percentagem da altura do ecrã a partir do topo onde um toque mostra a barra",
+ "reader.sb_header_button_size": "Tamanho dos botões da barra",
+ "reader.sb_header_button_hint": "Percentagem do tamanho predefinido (100 % = normal)",
"reader.sb_section_edge_pad": "Ajuste para bordas do ecrã",
"reader.sb_edge_pad_hint": "Para ecrãs curvos — afasta o conteúdo das bordas.",
"reader.sb_section_statusbar": "Barra de estado",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Espessura",
"reader.sb_book_prog": "Progresso do livro (barra)",
"reader.sb_chap_prog": "Progresso do capítulo (barra)",
+ "reader.sb_clock_format": "Formato da hora",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Dicionários",
"reader.sb_dicts_hint": "Escolher dicionários e ordem de pesquisa (↑↓)",
"reader.dict_no_dicts": "Sem dicionários. Adiciona ficheiros .ifo/.idx/.dict na pasta data/dictionaries/.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Sair de ecrã inteiro",
"reader.err_no_fullscreen": "Modo ecrã inteiro não disponível.",
"reader.sync_dlg_title": "Sincronização KOReader",
+ "reader.sync_dlg_hint": "Fechar esta caixa de diálogo saltará para a posição mais avançada para proteger o seu progresso.",
"reader.sync_dlg_col_device": "Dispositivo",
"reader.sync_dlg_col_pos": "Posição",
"reader.sync_dlg_col_time": "Hora",
@@ -362,8 +377,6 @@
"opds.sync_title": "Sincronizar para estante",
"opds.err_browse": "Erro de navegação: {msg}",
"opds.btn_add": "+ Adicionar à biblioteca",
- "opds.btn_read": "Ler",
- "opds.btn_peek": "Pré-visualizar",
"opds.no_description": "Sem descrição.",
"opds.btn_downloading": "A descarregar…",
"opds.toast_book_added": "\"{title}\" adicionado à biblioteca.",
@@ -394,10 +407,11 @@
"opds.page_next": "Próxima",
"opds.page_info": "Página {page} · {count} livros",
"reader.dict_loading": "A pesquisar…",
+ "reader.dict_lookup_chip": "Consultar",
"error.session_expired": "Sessão expirada. Por favor, inicie sessão novamente.",
"error.http_error": "Erro {status}",
"error.credentials_required": "Nome de utilizador e senha são obrigatórios.",
- "error.username_invalid": "O nome de utilizador deve ter 3\u201332 caracteres.",
+ "error.username_invalid": "O nome de utilizador deve ter 3–32 caracteres.",
"error.password_too_short": "A senha deve ter pelo menos 8 caracteres.",
"error.registration_disabled": "O registo de novos utilizadores está desativado.",
"error.username_taken": "O nome de utilizador já está em uso.",
@@ -496,5 +510,6 @@
"reader.no_custom_fonts": "Sem fontes personalizadas",
"reader.upload_fonts": "Carregar fontes…",
"reader.upload_dict": "Carregar dicionário ZIP…",
- "reader.uploading": "Carregando"
+ "reader.uploading": "Carregando",
+ "reader.sb_battery": "Bateria"
}
diff --git a/public/locales/sl.json b/public/locales/sl.json
index 96eda5c..9f6e9f6 100644
--- a/public/locales/sl.json
+++ b/public/locales/sl.json
@@ -51,6 +51,7 @@
"library.sort_title_za": "Naslov Z–A",
"library.sort_author_az": "Avtor A–Z",
"library.sort_progress": "Napredek",
+ "library.sort_last_opened": "Nazadnje odprto",
"library.sort_series": "Serija",
"library.btn_edit": "Uredi",
"library.add_book": "Dodaj knjigo",
@@ -92,7 +93,6 @@
"library.btn_del_book": "Izbriši",
"library.btn_download": "Prenesi",
"library.btn_read": "Beri",
- "library.btn_peek": "Predogled",
"library.btn_cover_preview": "Predogled naslovnice",
"library.btn_cover_info": "Podrobnosti knjige",
"library.btn_cover_delete": "Izbriši knjigo",
@@ -195,6 +195,8 @@
"reader.sb_skip_open_hint": "Če je vključeno, knjiga se vedno odpre na začetku brez preverjanja shranjenega/sinhroniziranega položaja",
"reader.sb_skip_close": "Ne shrani ob zaprtju knjige",
"reader.sb_skip_close_hint": "Če je vključeno, ob vrnitvi v knjižnico ali zaprtju zavihka se položaj ne shrani samodejno",
+ "reader.sb_save_on_hide": "Shrani ob zaklepu zaslona / preklopu aplikacije",
+ "reader.sb_save_on_hide_hint": "Pošlji položaj branja na strežnik, ko zaklenete zaslon ali preklopite na drugo aplikacijo.",
"reader.sb_hyphenation": "Deljenje besed",
"reader.sb_hyphenation_hint": "Samodejno deljenje besed na koncu vrstice",
"reader.sb_bionic": "Bionično branje",
@@ -203,6 +205,15 @@
"reader.sb_hyphenation_lang_auto": "Samodejno (iz knjige)",
"reader.sb_page_shadow": "Rob strani (senca knjige)",
"reader.sb_page_shadow_hint": "Temni rob med stranema v načinu dveh strani",
+ "reader.sb_section_navigation": "Navigacija po straneh",
+ "reader.sb_nav_zone_left": "Cona nazaj (levi rob)",
+ "reader.sb_nav_zone_right": "Cona naprej (desni rob)",
+ "reader.sb_nav_zone_hint": "Tapnite ali kliknite ta del zaslona za obračanje strani.",
+ "reader.sb_section_toolbar": "Orodna vrstica",
+ "reader.sb_header_reveal_zone": "Cona za prikaz orodne vrstice",
+ "reader.sb_header_reveal_hint": "Odstotek višine zaslona od vrha, kjer tap prikaže orodno vrstico",
+ "reader.sb_header_button_size": "Velikost gumbov orodne vrstice",
+ "reader.sb_header_button_hint": "Odstotek privzete velikosti gumbov (100 % = normalno)",
"reader.sb_section_edge_pad": "Odmik robov strani",
"reader.sb_edge_pad_hint": "Za zaobljene robove zaslona (telefon) — premakne vsebino in statusno vrstico od roba.",
"reader.sb_section_statusbar": "Statusna vrstica",
@@ -213,6 +224,9 @@
"reader.sb_sep_thickness": "Debelina",
"reader.sb_book_prog": "Napredek knjige (trak)",
"reader.sb_chap_prog": "Napredek poglavja (trak)",
+ "reader.sb_clock_format": "Format ure",
+ "reader.sb_clock_24h": "24h",
+ "reader.sb_clock_12h": "12h (AM/PM)",
"reader.sb_dicts": "Slovarji",
"reader.sb_dicts_hint": "Izberite slovarje in nastavite vrstni red iskanja (↑↓)",
"reader.dict_no_dicts": "Ni slovarjev. Dodajte .ifo/.idx/.dict datoteke v mapo data/dictionaries/.",
@@ -248,6 +262,7 @@
"reader.btn_fullscreen_exit": "Izhod iz celozaslonskega načina",
"reader.err_no_fullscreen": "Celozaslonski način ni na voljo.",
"reader.sync_dlg_title": "Sinhronizacija KOReader",
+ "reader.sync_dlg_hint": "Če zaprete to pogovorno okno, bo bralnik skočil na naprednejši položaj, da zaščiti vaš napredek.",
"reader.sync_dlg_col_device": "Naprava",
"reader.sync_dlg_col_pos": "Položaj",
"reader.sync_dlg_col_time": "Čas",
@@ -362,8 +377,6 @@
"opds.sync_title": "Sinhroniziraj v polico",
"opds.err_browse": "Napaka pri brskanju: {msg}",
"opds.btn_add": "+ Dodaj v knjižnico",
- "opds.btn_read": "Beri",
- "opds.btn_peek": "Predogled",
"opds.no_description": "Ni opisa.",
"opds.btn_downloading": "Prenašam…",
"opds.toast_book_added": "\"{title}\" dodana v knjižnico.",
@@ -394,6 +407,7 @@
"opds.page_next": "Naslednja",
"opds.page_info": "Stran {page} · {count} knjig",
"reader.dict_loading": "Iščem…",
+ "reader.dict_lookup_chip": "Poišči",
"error.session_expired": "Seja je potekla. Prijavite se znova.",
"error.http_error": "Napaka {status}",
"error.credentials_required": "Uporabniško ime in geslo sta obvezna.",
@@ -496,5 +510,6 @@
"reader.no_custom_fonts": "Ni pisav po meri",
"reader.upload_fonts": "Naloži pisave…",
"reader.upload_dict": "Naloži slovar ZIP…",
- "reader.uploading": "Nalaganje"
+ "reader.uploading": "Nalaganje",
+ "reader.sb_battery": "Baterija"
}
diff --git a/public/login.html b/public/login.html
index 559fc95..b86d973 100644
--- a/public/login.html
+++ b/public/login.html
@@ -136,10 +136,6 @@
-
+