Reader: schnelleres j/k-Scrollen; Offline-Modus auf Desktop repariert (SW v33)
j/k nutzte ein starres 8px/16ms-setInterval (~500px/s, unsauber getaktet gegenüber dem Frame-Takt) — ersetzt durch eine requestAnimationFrame-Schleife mit sanftem Ramp-up (700 -> 1800px/s über 300ms), das behebt sowohl die zu niedrige Geschwindigkeit als auch das Ruckeln kurz nach Scrollbeginn. Der Service Worker war unter /static/js/sw.js registriert, wodurch sein Scope (mangels Service-Worker-Allowed-Header) automatisch auf /static/js/ beschränkt war statt die ganze App abzudecken — Navigationen zu z.B. / liefen nie durch den fetch-Handler, weshalb offline nur die Browser-eigene Fehlerseite statt der App-Shell erschien. Jetzt wird sw.js zusätzlich unter der Root /sw.js ausgeliefert (diora/urls.py) und dort mit explizitem scope:'/' registriert; alte Registrierungen mit dem falschen Scope werden beim Laden automatisch entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d3c435dd59
commit
e70bb881cc
3 changed files with 46 additions and 12 deletions
|
|
@ -2,6 +2,7 @@ from django.conf import settings
|
|||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.static import serve as serve_static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
|
@ -9,5 +10,11 @@ urlpatterns = [
|
|||
path('podcasts/', include('podcasts.urls')),
|
||||
path('books/', include('books.urls')),
|
||||
path('api/2/', include('gpodder.urls')),
|
||||
# Served at the root (not /static/js/sw.js) so its default scope covers
|
||||
# the whole app — a service worker's scope is limited to its own script's
|
||||
# directory unless the server sends Service-Worker-Allowed, so registering
|
||||
# it from under /static/js/ silently restricted it to that subpath and
|
||||
# navigations to '/', '/books/' etc. were never intercepted while offline.
|
||||
path('sw.js', serve_static, {'document_root': settings.BASE_DIR / 'static' / 'js', 'path': 'sw.js'}),
|
||||
path('', include('radio.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
|||
|
|
@ -2237,14 +2237,16 @@ document.addEventListener('keydown', e => {
|
|||
const contentEl = $('reader-content');
|
||||
if (contentEl) {
|
||||
const large = Math.round(contentEl.clientHeight * 0.85);
|
||||
const continuousKeys = {'j': 8, 'k': -8};
|
||||
const continuousKeys = {'j': 1, 'k': -1};
|
||||
if (continuousKeys[e.key] !== undefined && !e.repeat) {
|
||||
e.preventDefault();
|
||||
_readerScrollStep = continuousKeys[e.key];
|
||||
if (!_readerScrollInterval) {
|
||||
_readerScrollInterval = setInterval(() => {
|
||||
$('reader-content')?.scrollBy({top: _readerScrollStep});
|
||||
}, 16);
|
||||
const dir = continuousKeys[e.key];
|
||||
const now = performance.now();
|
||||
if (_readerScrollDir !== dir) _readerScrollStartTs = now;
|
||||
_readerScrollDir = dir;
|
||||
_readerScrollLastTs = now;
|
||||
if (!_readerScrollRaf) {
|
||||
_readerScrollRaf = requestAnimationFrame(_readerScrollTick);
|
||||
}
|
||||
}
|
||||
if (e.key === 'd') { e.preventDefault(); contentEl.scrollBy({top: large / 2, behavior: 'smooth'}); }
|
||||
|
|
@ -2264,8 +2266,9 @@ document.addEventListener('keydown', e => {
|
|||
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'j' || e.key === 'k') {
|
||||
clearInterval(_readerScrollInterval);
|
||||
_readerScrollInterval = null;
|
||||
if (_readerScrollRaf) cancelAnimationFrame(_readerScrollRaf);
|
||||
_readerScrollRaf = null;
|
||||
_readerScrollDir = 0;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2309,9 +2312,19 @@ function formatDuration(seconds) {
|
|||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/static/js/sw.js').catch(err => {
|
||||
// Registered from '/sw.js' (root), not '/static/js/sw.js': a service
|
||||
// worker's default scope is its own script's directory, so registering
|
||||
// it from under /static/js/ would only ever let it control requests
|
||||
// under that path — page navigations to '/', '/books/' etc. would never
|
||||
// be intercepted, breaking offline entirely.
|
||||
navigator.serviceWorker.register('/sw.js', {scope: '/'}).catch(err => {
|
||||
console.warn('Service worker registration failed:', err);
|
||||
});
|
||||
// Drop any leftover registration from the old, wrongly-scoped '/static/js/sw.js'
|
||||
// path so it doesn't keep running or fight the new root-scoped worker.
|
||||
navigator.serviceWorker.getRegistrations().then(regs => {
|
||||
regs.forEach(r => { if (r.scope.endsWith('/static/js/')) r.unregister(); });
|
||||
});
|
||||
});
|
||||
// When a new SW activates it sends SW_ACTIVATED — reload to get fresh assets,
|
||||
// but only if the reader isn't open (would interrupt reading).
|
||||
|
|
@ -3176,8 +3189,22 @@ let pdfTotalPages = 0;
|
|||
let _pdfPageTextBoxCache = {};
|
||||
let _pdfRenderGen = 0;
|
||||
let _touchStartX = 0;
|
||||
let _readerScrollInterval = null;
|
||||
let _readerScrollStep = 0;
|
||||
let _readerScrollRaf = null;
|
||||
let _readerScrollDir = 0;
|
||||
let _readerScrollStartTs = 0;
|
||||
let _readerScrollLastTs = 0;
|
||||
const READER_SCROLL_MIN_SPEED = 700; // px/s right as j/k is pressed
|
||||
const READER_SCROLL_MAX_SPEED = 1800; // px/s once ramped up
|
||||
const READER_SCROLL_RAMP_MS = 300; // time to reach max speed
|
||||
function _readerScrollTick(ts) {
|
||||
const dt = ts - _readerScrollLastTs;
|
||||
_readerScrollLastTs = ts;
|
||||
const held = ts - _readerScrollStartTs;
|
||||
const t = Math.min(held / READER_SCROLL_RAMP_MS, 1);
|
||||
const speed = READER_SCROLL_MIN_SPEED + (READER_SCROLL_MAX_SPEED - READER_SCROLL_MIN_SPEED) * t;
|
||||
$('reader-content')?.scrollBy({top: _readerScrollDir * speed * dt / 1000});
|
||||
_readerScrollRaf = requestAnimationFrame(_readerScrollTick);
|
||||
}
|
||||
let _pinchStartDist = 0;
|
||||
let _pinchStartZoom = 100;
|
||||
let _isPinching = false;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v32';
|
||||
const CACHE = 'diora-v33';
|
||||
const PODCAST_CACHE = 'diora-podcast-v1';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue