diff --git a/static/js/app/53-library.js b/static/js/app/53-library.js index ce52f4a..db1cc15 100644 --- a/static/js/app/53-library.js +++ b/static/js/app/53-library.js @@ -199,21 +199,53 @@ function _libCard(b, label) { ${_libEsc(by)}`; } +// ---- "Zuletzt gelesen": the book read last, large, and the ones read before it in a row -------- + function _libContinueBook(books) { if (_lib.status !== 'all' || _lib.folder != null || _lib.q || _lib.selecting) return null; return _recentlyReadBooks(books).filter(x => !x.is_read && x.keyOk !== false)[0] || null; } -function _libContinue(b) { +// The others read lately (finished ones too), newest first, without the large one. +function _libRecentBooks(books, going) { + if (!going) return []; + return _recentlyReadBooks(books).filter(x => x !== going && x.keyOk !== false).slice(0, 6); +} + +// "vor 5 Minuten", "gestern", "vor 3 Tagen", "am 4. Okt." +function _libAgo(iso) { + const t = Date.parse(iso || ''); + if (!t) return ''; + const min = Math.round((Date.now() - t) / 60000); + if (min < 2) return 'gerade eben'; + if (min < 60) return `vor ${min} Minuten`; + const h = Math.round(min / 60); + const day = d => new Date(d).toDateString(); + if (day(t) === day(Date.now())) return h === 1 ? 'vor einer Stunde' : `vor ${h} Stunden`; + if (day(t) === day(Date.now() - 86400000)) return 'gestern'; + const days = Math.round((Date.now() - t) / 86400000); + if (days < 7) return `vor ${days} Tagen`; + return 'am ' + new Date(t).toLocaleDateString('de-DE', {day: 'numeric', month: 'short'}); +} + +function _libContinue(b, recent) { if (!b) return ''; const pages = b.pages ? ` · Seite ${Math.max(1, Math.round(b.pages * (b.scroll_fraction || 0)))} von ${b.pages}` : ''; - return `
${_libCover(b, 'lib-thumb lib-thumb-l')} - Weiterlesen - ${_libEsc(b.title)} - ${_libEsc(b.author || '')} - - ${_libPct(b)} %${pages} -
`; + const ago = _libAgo(b.last_read); + const row = (recent || []).map(r => `
+ ${_libCover(r, 'lib-cover')}${r.is_read ? '✓' : ''} + + ${_libEsc(r.title)}
`).join(''); + return `
+
${_libCover(b, 'lib-hero-cover')} + Weiterlesen${ago ? ' · ' + ago : ''} + ${_libEsc(b.title)} + ${_libEsc(b.author || '')} + + ${_libPct(b)} %${pages} +
+ ${row ? `
Zuletzt gelesen
${row}
` : ''} +
`; } function _libVolumeLabel(b) { @@ -312,7 +344,7 @@ function _libRender(allBooks) { } else { const going = _libContinueBook(books); const pool = shown.slice(); - h += _libContinue(going); + h += _libContinue(going, _libRecentBooks(books, going)); if (going) shown.splice(shown.indexOf(going), 1); // once, on top // Series as stacks, unless searching, filtering by state or choosing several. const stack = !magazines && !_lib.q && _lib.status === 'all' && !_lib.selecting; @@ -697,9 +729,24 @@ function _libAddMenu() { '#book-list .lib-btn, .lib-sheet-item { min-height: 36px; padding: 0 14px; border-radius: 18px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }', '.lib-sheet-item { display: block; width: 100%; text-align: left; border: none; border-radius: 0; border-bottom: 1px solid var(--border, #2a2a2a); min-height: 46px; }', '.lib-danger { color: var(--accent, #e63946); }', - '#book-list .lib-continue { display: flex; align-items: center; gap: 12px; padding: 10px; margin: 0 0 16px; border: 1px solid var(--border, #2a2a2a); border-radius: 10px; background: var(--card, #0b0b0b); cursor: pointer; }', '#book-list .lib-kicker { color: var(--accent, #e63946); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }', - '#book-list .lib-play { flex: none; width: 46px; height: 46px; border-radius: 50%; background: var(--accent, #e63946); color: var(--on-accent, #fff); display: flex; align-items: center; justify-content: center; font-size: 17px; }', + // "Zuletzt gelesen": the last book large (cover, title, progress, a button), the others in a row. + '#book-list .lib-recent { margin: 0 0 22px; }', + '#book-list .lib-continue { display: flex; align-items: stretch; gap: 16px; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 12px; background: var(--card, #0b0b0b); cursor: pointer; }', + '#book-list .lib-hero-cover { flex: none; width: 104px; height: 156px; border-radius: 5px; object-fit: cover; box-shadow: 0 2px 8px rgba(0,0,0,.35); }', + '#book-list .lib-hero-cover.lib-nocover { padding: 8px; }', + '#book-list .lib-continue .lib-text { justify-content: center; gap: 4px; }', + '#book-list .lib-continue .lib-hero-title { font-size: 18px; font-weight: 700; line-height: 1.25; -webkit-line-clamp: 3; }', + '#book-list .lib-hero-progress { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }', + '#book-list .lib-hero-progress .lib-bar { height: 5px; margin-top: 0; }', + '#book-list .lib-play { align-self: flex-start; margin-top: 10px; padding: 9px 18px; border-radius: 20px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 14px; font-weight: 600; white-space: nowrap; }', + '@media (min-width: 721px) { #book-list .lib-hero-cover { width: 136px; height: 204px; } #book-list .lib-continue .lib-hero-title { font-size: 22px; } #book-list .lib-continue { padding: 18px; gap: 22px; } }', + '#book-list .lib-recent-head { margin: 16px 0 8px; font-size: 13px; color: var(--muted, #9a9a9a); }', + '#book-list .lib-recent-row { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }', + '#book-list .lib-recent-row::-webkit-scrollbar { display: none; }', + '#book-list .lib-recent-item { flex: none; width: 92px; display: flex; flex-direction: column; gap: 3px; cursor: pointer; }', + '@media (min-width: 721px) { #book-list .lib-recent-item { width: 112px; } }', + '#book-list .lib-recent-item .lib-title { font-size: 12px; }', '#book-list .lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 18px 14px; }', '@media (max-width: 480px) { #book-list .lib-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 10px; } }', '#book-list .lib-card { display: flex; flex-direction: column; gap: 3px; min-width: 0; cursor: pointer; }', @@ -732,6 +779,11 @@ function _libAddMenu() { '#book-list .lib-count { opacity: .7; }', '#book-list .lib-empty { color: var(--muted, #9a9a9a); font-size: 14px; }', 'body.eink #book-list .lib-add, body.eink #book-list .lib-play { background: #000; color: #fff; }', + 'body.eink #book-list .lib-continue { border: 2px solid #000; background: #fff; }', + 'body.eink #book-list .lib-hero-cover { box-shadow: none; border: 1px solid #000; }', + // Progress on e-ink: a light track, the part read in black (the border colour is black there). + 'body.eink #book-list .lib-bar { background: #ccc; }', + 'body.eink #book-list .lib-bar > span { background: #000; }', '#tab-books.lib-drop { outline: 2px dashed var(--accent, #e63946); outline-offset: -6px; }', '#book-list .lib-stackbox::before, #book-list .lib-stackbox::after { content: ""; position: absolute; left: 7%; right: 7%; height: 100%; border-radius: 4px; border: 1px solid var(--border-strong, #444); background: var(--panel, #161616); }', '#book-list .lib-stackbox::before { top: -9px; left: 14%; right: 14%; }', diff --git a/static/js/sw.js b/static/js/sw.js index c4ec04f..8947bb6 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v124'; +const CACHE = 'diora-v125'; const SHELL = [ '/static/css/app.css', '/static/js/app/01-core.js',