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}
+ ▶ Weiterlesen
+ ${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',