From 0a527767b0cb529110930018f8b352d8831aec5c Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:05:43 -0400 Subject: [PATCH] Docs: document the Books shelf rows and its new sizes The widget holds a shelf row per size instead of one view, and offers a medium and a large size. The page named one size and described the view settings as belonging to the widget. Split the Configuration table so the per-shelf fields stand on their own, and refresh the embedded widget copies. Move both preview stubs onto the shelves payload the widget now reads, without which the previews draw an empty shelf. --- public/api/widget-config/w-books | 2 +- public/api/widget-config/w-books-md | 2 +- public/api/widget-data/w-books | 44 ++++++- public/api/widget-data/w-books-md | 98 +++++++++++++-- public/js/admin-list.js | 2 +- public/js/admin-widget-form.js | 2 +- public/js/admin.js | 4 +- public/js/widget-config-form.js | 12 +- public/widgets/books/data.js | 152 +++++++++++++----------- public/widgets/books/demo.js | 83 +++++++------ public/widgets/books/i18n/de.json | 1 + public/widgets/books/i18n/en.json | 1 + public/widgets/books/i18n/es.json | 1 + public/widgets/books/i18n/fa.json | 1 + public/widgets/books/i18n/fr.json | 1 + public/widgets/books/i18n/zh-Hans.json | 1 + public/widgets/books/index.html | 48 ++++++-- public/widgets/books/widget.json | 66 ++++++---- src/content/docs/docs/widgets/books.mdx | 13 +- 19 files changed, 368 insertions(+), 166 deletions(-) diff --git a/public/api/widget-config/w-books b/public/api/widget-config/w-books index 61c5760..7103a53 100644 --- a/public/api/widget-config/w-books +++ b/public/api/widget-config/w-books @@ -1 +1 @@ -{"widgetSize":"small","widgetConfig":{"provider":"audiobookshelf","absUrl":"https://books.example.com","source":"unread","href":"https://books.example.com"}} \ No newline at end of file +{"widgetSize": "small", "widgetConfig": {"provider": "audiobookshelf", "absUrl": "https://books.example.com", "href": "https://books.example.com", "shelves": [{"source": "unread"}]}} diff --git a/public/api/widget-config/w-books-md b/public/api/widget-config/w-books-md index e385415..7015cbe 100644 --- a/public/api/widget-config/w-books-md +++ b/public/api/widget-config/w-books-md @@ -1 +1 @@ -{"widgetSize": "medium", "widgetConfig": {"provider": "audiobookshelf", "absUrl": "https://books.example.com", "source": "unread", "href": "https://books.example.com"}} \ No newline at end of file +{"widgetSize": "medium", "widgetConfig": {"provider": "audiobookshelf", "absUrl": "https://books.example.com", "href": "https://books.example.com", "shelves": [{"source": "unread"}]}} diff --git a/public/api/widget-data/w-books b/public/api/widget-data/w-books index e29ed98..5e4191b 100644 --- a/public/api/widget-data/w-books +++ b/public/api/widget-data/w-books @@ -1,10 +1,42 @@ { "provider": "audiobookshelf", - "source": "list", - "books": [ - { "title": "Dune", "author": "Frank Herbert", "progress": 0.42, "finished": false, "color": "#A8622A", "kind": "book" }, - { "title": "Project Hail Mary", "author": "Andy Weir", "progress": 0.77, "finished": false, "color": "#1D4E6B", "kind": "book" }, - { "title": "The Hobbit", "author": "J. R. R. Tolkien", "progress": 1, "finished": true, "color": "#2F5D3A", "kind": "book" }, - { "title": "Neuromancer", "author": "William Gibson", "progress": null, "finished": false, "color": "#6E2B4F", "kind": "book" } + "shelves": [ + { + "source": "list", + "books": [ + { + "title": "Dune", + "author": "Frank Herbert", + "progress": 0.42, + "finished": false, + "color": "#A8622A", + "kind": "book" + }, + { + "title": "Project Hail Mary", + "author": "Andy Weir", + "progress": 0.77, + "finished": false, + "color": "#1D4E6B", + "kind": "book" + }, + { + "title": "The Hobbit", + "author": "J. R. R. Tolkien", + "progress": 1, + "finished": true, + "color": "#2F5D3A", + "kind": "book" + }, + { + "title": "Neuromancer", + "author": "William Gibson", + "progress": null, + "finished": false, + "color": "#6E2B4F", + "kind": "book" + } + ] + } ] } diff --git a/public/api/widget-data/w-books-md b/public/api/widget-data/w-books-md index 8db9096..4758016 100644 --- a/public/api/widget-data/w-books-md +++ b/public/api/widget-data/w-books-md @@ -1,16 +1,90 @@ { "provider": "komga", - "source": "recently", - "books": [ - { "title": "Saga", "author": "Brian K. Vaughan", "progress": 0.31, "finished": false, "color": "#9B2F2F", "kind": "series" }, - { "title": "Watchmen", "author": "Alan Moore", "progress": 0.64, "finished": false, "color": "#C8A21C", "kind": "series" }, - { "title": "Berserk", "author": "Kentaro Miura", "progress": 1, "finished": true, "color": "#33363B", "kind": "series" }, - { "title": "Monstress", "author": "Marjorie Liu", "progress": null, "finished": false, "color": "#4A3A7A", "kind": "series" }, - { "title": "Akira", "author": "Katsuhiro Otomo", "progress": 0.18, "finished": false, "color": "#1F6F6B", "kind": "series" }, - { "title": "Vinland Saga", "author": "Makoto Yukimura", "progress": 0.52, "finished": false, "color": "#7A4A1F", "kind": "series" }, - { "title": "Blacksad", "author": "Juan Diaz Canales", "progress": 0.87, "finished": false, "color": "#264F7A", "kind": "series" }, - { "title": "Sandman", "author": "Neil Gaiman", "progress": 1, "finished": true, "color": "#5C2C6E", "kind": "series" }, - { "title": "Pluto", "author": "Naoki Urasawa", "progress": 0.05, "finished": false, "color": "#A34E1C", "kind": "series" }, - { "title": "Nausicaa", "author": "Hayao Miyazaki", "progress": null, "finished": false, "color": "#3F6B32", "kind": "series" } + "shelves": [ + { + "source": "recently", + "books": [ + { + "title": "Saga", + "author": "Brian K. Vaughan", + "progress": 0.31, + "finished": false, + "color": "#9B2F2F", + "kind": "series" + }, + { + "title": "Watchmen", + "author": "Alan Moore", + "progress": 0.64, + "finished": false, + "color": "#C8A21C", + "kind": "series" + }, + { + "title": "Berserk", + "author": "Kentaro Miura", + "progress": 1, + "finished": true, + "color": "#33363B", + "kind": "series" + }, + { + "title": "Monstress", + "author": "Marjorie Liu", + "progress": null, + "finished": false, + "color": "#4A3A7A", + "kind": "series" + }, + { + "title": "Akira", + "author": "Katsuhiro Otomo", + "progress": 0.18, + "finished": false, + "color": "#1F6F6B", + "kind": "series" + }, + { + "title": "Vinland Saga", + "author": "Makoto Yukimura", + "progress": 0.52, + "finished": false, + "color": "#7A4A1F", + "kind": "series" + }, + { + "title": "Blacksad", + "author": "Juan Diaz Canales", + "progress": 0.87, + "finished": false, + "color": "#264F7A", + "kind": "series" + }, + { + "title": "Sandman", + "author": "Neil Gaiman", + "progress": 1, + "finished": true, + "color": "#5C2C6E", + "kind": "series" + }, + { + "title": "Pluto", + "author": "Naoki Urasawa", + "progress": 0.05, + "finished": false, + "color": "#A34E1C", + "kind": "series" + }, + { + "title": "Nausicaa", + "author": "Hayao Miyazaki", + "progress": null, + "finished": false, + "color": "#3F6B32", + "kind": "series" + } + ] + } ] } diff --git a/public/js/admin-list.js b/public/js/admin-list.js index d01f87f..9207a00 100644 --- a/public/js/admin-list.js +++ b/public/js/admin-list.js @@ -19,7 +19,7 @@ import { paintIcon } from '/js/admin-shared.js?v=132c869f'; import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=d949e985'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { t } from '/js/i18n.js?v=e644a5c5'; -import { sizeLabel } from '/js/admin-widget-form.js?v=db272c65'; +import { sizeLabel } from '/js/admin-widget-form.js?v=0ca20e21'; import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947'; /** @type {{ openModal: (idx: number|null) => void, diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js index 10f9cb1..1734981 100644 --- a/public/js/admin-widget-form.js +++ b/public/js/admin-widget-form.js @@ -1,6 +1,6 @@ import { state } from '/js/admin-state.js?v=7d68e98e'; import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=132c869f'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=ccaf1acf'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=0f95ec4c'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=dcf7c37d'; import { t } from '/js/i18n.js?v=e644a5c5'; diff --git a/public/js/admin.js b/public/js/admin.js index 2127cc4..de4b999 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,6 +1,6 @@ import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=5702feef'; import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=9fc96f28'; -import { initList, render, syncFilterUI } from '/js/admin-list.js?v=cea8d7ce'; +import { initList, render, syncFilterUI } from '/js/admin-list.js?v=0a1e50b5'; import { resolveAdminSection } from '/js/admin-logic.js?v=dcf7c37d'; import { buildAppItem, @@ -13,7 +13,7 @@ import { import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=f019211c'; import { ag, ap, initInlineEdit, paintIcon, setReauthHandler, toast } from '/js/admin-shared.js?v=132c869f'; import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=db272c65'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=0ca20e21'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=e644a5c5'; import { loadLocalIcons } from '/js/icons.js?v=69c2b9bd'; diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js index de61b94..d4841af 100644 --- a/public/js/widget-config-form.js +++ b/public/js/widget-config-form.js @@ -530,6 +530,7 @@ function _object(field, value, ctx) { if (sf.type === 'group' || sf.type === 'object') continue; const b = _buildSimple(sf, cfg, subCtx); b.field = sf; + if (!b.el.classList.contains('row')) b.el.classList.add('row-wrap'); card.appendChild(b.el); built.push(b); } @@ -614,6 +615,7 @@ function _group(field, rows, size, ctx) { if (sf.type === 'group' || sf.type === 'object') continue; const b = _buildSimple(sf, rowData, rowCtx); b.field = sf; + if (!b.el.classList.contains('row')) b.el.classList.add('row-wrap'); card.appendChild(b.el); built.push(b); } @@ -686,9 +688,14 @@ export function renderWidgetConfigForm(container, fields, config = {}, opts = {} }; for (const f of fields) { if (!f || !f.key) continue; - if (f.type === 'group' || f.type === 'object') { + if (f.type === 'group' || f.type === 'object' || f.type === 'picklist') { flush(); - const b = f.type === 'group' ? _group(f, config[f.key], opts && opts.size, ctx) : _object(f, config[f.key], ctx); + const b = + f.type === 'group' + ? _group(f, config[f.key], opts && opts.size, ctx) + : f.type === 'object' + ? _object(f, config[f.key], ctx) + : _picklist(f, config[f.key], ctx, ctx.size); b.field = f; container.appendChild(b.el); built.push(b); @@ -701,6 +708,7 @@ export function renderWidgetConfigForm(container, fields, config = {}, opts = {} card.className = 'grp'; container.appendChild(card); } + if (!b.el.classList.contains('row')) b.el.classList.add('row-wrap'); card.appendChild(b.el); if (b.control) b.control.addEventListener('change', () => notify(f.key)); built.push(b); diff --git a/public/widgets/books/data.js b/public/widgets/books/data.js index 0968ae2..a95d1a4 100644 --- a/public/widgets/books/data.js +++ b/public/widgets/books/data.js @@ -1,4 +1,4 @@ -const CAP = 8; +const CAP = 16; const enc = encodeURIComponent; const clamp01 = n => Math.min(1, Math.max(0, n)); const cap = arr => (Array.isArray(arr) ? arr : []).slice(0, CAP); @@ -20,6 +20,11 @@ function jpost(ctx, base, path, headers, body) { function authErr(r) { return r.status === 401 || r.status === 403; } +function shelvesOf(ctx) { + const rows = Array.isArray(ctx.config.shelves) ? ctx.config.shelves : null; + const list = rows && rows.length ? rows : [{ source: ctx.config.source, listId: ctx.config.listId }]; + return list.map(r => ({ source: (r && r.source) || 'recently', listId: r && r.listId })); +} /* ───────────────────────── Audiobookshelf ───────────────────────── */ async function absLibrary(ctx, base, hdr) { @@ -42,24 +47,17 @@ function absBook(li) { kind: 'book', }; } -async function abs(ctx) { - const base = ctx.normalizeBase(ctx.config.absUrl); - const key = ctx.config.absKey; - if (!base || !key) ctx.fail('Audiobookshelf URL and API key required', { kind: ctx.KIND.INVALID }); - const hdr = { Authorization: 'Bearer ' + key }; - const lib = await absLibrary(ctx, base, hdr); - const source = ctx.config.source || 'recently'; - let items = []; - if (source === 'list' && ctx.config.listId) { - const [kind, id] = String(ctx.config.listId).split(':'); +async function absItems(ctx, base, hdr, lib, shelf) { + if (shelf.source === 'list' && shelf.listId) { + const [kind, id] = String(shelf.listId).split(':'); if (kind === 'playlist') { const r = await jget(ctx, base, `/api/playlists/${enc(id)}`, hdr); - items = ((r.data && r.data.items) || []).map(it => it.libraryItem || it); - } else { - const r = await jget(ctx, base, `/api/collections/${enc(id)}`, hdr); - items = (r.data && r.data.books) || []; + return ((r.data && r.data.items) || []).map(it => it.libraryItem || it); } - } else if (source === 'unread') { + const r = await jget(ctx, base, `/api/collections/${enc(id)}`, hdr); + return (r.data && r.data.books) || []; + } + if (shelf.source === 'unread') { const f = Buffer.from('not-finished').toString('base64'); const r = await jget( ctx, @@ -67,18 +65,28 @@ async function abs(ctx) { `/api/libraries/${enc(lib)}/items?filter=progress.${f}&sort=addedAt&desc=1&limit=${CAP}`, hdr, ); - items = (r.data && r.data.results) || []; - } else { - const r = await jget(ctx, base, `/api/libraries/${enc(lib)}/items?sort=addedAt&desc=1&limit=${CAP}`, hdr); - items = (r.data && r.data.results) || []; + return (r.data && r.data.results) || []; } - return { - provider: 'audiobookshelf', - source, - books: cap(items) - .map(absBook) - .filter(b => b.title), - }; + const r = await jget(ctx, base, `/api/libraries/${enc(lib)}/items?sort=addedAt&desc=1&limit=${CAP}`, hdr); + return (r.data && r.data.results) || []; +} +async function abs(ctx) { + const base = ctx.normalizeBase(ctx.config.absUrl); + const key = ctx.config.absKey; + if (!base || !key) ctx.fail('Audiobookshelf URL and API key required', { kind: ctx.KIND.INVALID }); + const hdr = { Authorization: 'Bearer ' + key }; + const lib = await absLibrary(ctx, base, hdr); + const shelves = []; + for (const shelf of shelvesOf(ctx)) { + const items = await absItems(ctx, base, hdr, lib, shelf); + shelves.push({ + source: shelf.source, + books: cap(items) + .map(absBook) + .filter(b => b.title), + }); + } + return { provider: 'audiobookshelf', shelves }; } async function absLists(ctx) { const base = ctx.normalizeBase(ctx.config.absUrl); @@ -119,21 +127,23 @@ async function komga(ctx) { const key = ctx.config.komgaKey; if (!base || !key) ctx.fail('Komga URL and API key required', { kind: ctx.KIND.INVALID }); const hdr = { 'X-API-Key': key }; - const source = ctx.config.source || 'recently'; - let path; - if (source === 'list' && ctx.config.listId) path = `/api/v1/readlists/${enc(ctx.config.listId)}/books?size=${CAP}`; - else if (source === 'unread') path = `/api/v1/books/ondeck?size=${CAP}`; - else path = `/api/v1/books/latest?size=${CAP}`; - const r = await jget(ctx, base, path, hdr); - if (authErr(r)) ctx.fail('Komga auth failed (check API key)', { kind: ctx.KIND.AUTH }); - const content = (r.data && r.data.content) || (Array.isArray(r.data) ? r.data : []); - return { - provider: 'komga', - source, - books: cap(content) - .map(komgaBook) - .filter(b => b.title), - }; + const shelves = []; + for (const shelf of shelvesOf(ctx)) { + let path; + if (shelf.source === 'list' && shelf.listId) path = `/api/v1/readlists/${enc(shelf.listId)}/books?size=${CAP}`; + else if (shelf.source === 'unread') path = `/api/v1/books/ondeck?size=${CAP}`; + else path = `/api/v1/books/latest?size=${CAP}`; + const r = await jget(ctx, base, path, hdr); + if (authErr(r)) ctx.fail('Komga auth failed (check API key)', { kind: ctx.KIND.AUTH }); + const content = (r.data && r.data.content) || (Array.isArray(r.data) ? r.data : []); + shelves.push({ + source: shelf.source, + books: cap(content) + .map(komgaBook) + .filter(b => b.title), + }); + } + return { provider: 'komga', shelves }; } async function komgaLists(ctx) { const base = ctx.normalizeBase(ctx.config.komgaUrl); @@ -162,41 +172,45 @@ function kavitaSeries(s) { kind: 'series', }; } -async function kavita(ctx) { - const base = ctx.normalizeBase(ctx.config.kavitaUrl); - const key = ctx.config.kavitaKey; - if (!base || !key) ctx.fail('Kavita URL and API key required', { kind: ctx.KIND.INVALID }); - const tok = await kavitaToken(ctx, base, key); - const hdr = { Authorization: 'Bearer ' + tok }; - const source = ctx.config.source || 'recently'; - let list = []; - if (source === 'list' && ctx.config.listId) { - const r = await jget(ctx, base, `/api/ReadingList/items?readingListId=${enc(ctx.config.listId)}`, hdr); - list = (Array.isArray(r.data) ? r.data : []).map(it => ({ +async function kavitaList(ctx, base, hdr, shelf) { + if (shelf.source === 'list' && shelf.listId) { + const r = await jget(ctx, base, `/api/ReadingList/items?readingListId=${enc(shelf.listId)}`, hdr); + return (Array.isArray(r.data) ? r.data : []).map(it => ({ name: it.seriesName || it.title, pages: it.pagesTotal, pagesRead: it.pagesRead, primaryColor: it.primaryColor, })); - } else if (source === 'unread') { + } + if (shelf.source === 'unread') { const r = await jpost(ctx, base, `/api/Series/on-deck?pageSize=${CAP}`, hdr, {}); - list = Array.isArray(r.data) ? r.data : []; - } else { - const r = await jpost(ctx, base, `/api/Series/recently-added-v2?pageSize=${CAP}`, hdr, { - statements: [], - combination: 1, - limitTo: CAP, - sortOptions: { sortField: 2, isAscending: false }, + return Array.isArray(r.data) ? r.data : []; + } + const r = await jpost(ctx, base, `/api/Series/recently-added-v2?pageSize=${CAP}`, hdr, { + statements: [], + combination: 1, + limitTo: CAP, + sortOptions: { sortField: 2, isAscending: false }, + }); + return Array.isArray(r.data) ? r.data : []; +} +async function kavita(ctx) { + const base = ctx.normalizeBase(ctx.config.kavitaUrl); + const key = ctx.config.kavitaKey; + if (!base || !key) ctx.fail('Kavita URL and API key required', { kind: ctx.KIND.INVALID }); + const tok = await kavitaToken(ctx, base, key); + const hdr = { Authorization: 'Bearer ' + tok }; + const shelves = []; + for (const shelf of shelvesOf(ctx)) { + const list = await kavitaList(ctx, base, hdr, shelf); + shelves.push({ + source: shelf.source, + books: cap(list) + .map(kavitaSeries) + .filter(b => b.title), }); - list = Array.isArray(r.data) ? r.data : []; } - return { - provider: 'kavita', - source, - books: cap(list) - .map(kavitaSeries) - .filter(b => b.title), - }; + return { provider: 'kavita', shelves }; } async function kavitaLists(ctx) { const base = ctx.normalizeBase(ctx.config.kavitaUrl); diff --git a/public/widgets/books/demo.js b/public/widgets/books/demo.js index f111583..02cd0e3 100644 --- a/public/widgets/books/demo.js +++ b/public/widgets/books/demo.js @@ -1,40 +1,53 @@ +const TITLES = [ + ['The Left Hand of Darkness', 'Ursula K. Le Guin'], + ['Piranesi', 'Susanna Clarke'], + ['The Dispossessed', 'Ursula K. Le Guin'], + ['Klara and the Sun', 'Kazuo Ishiguro'], + ['A Memory Called Empire', 'Arkady Martine'], + ['The Vanished Birds', 'Simon Jimenez'], + ['Station Eleven', 'Emily St. John Mandel'], + ['The City and the City', 'China Mieville'], + ['Solaris', 'Stanislaw Lem'], + ['The Fifth Season', 'N. K. Jemisin'], + ['Never Let Me Go', 'Kazuo Ishiguro'], + ['Roadside Picnic', 'Arkady and Boris Strugatsky'], + ['Annihilation', 'Jeff VanderMeer'], + ['The Doors of Eden', 'Adrian Tchaikovsky'], + ['Exhalation', 'Ted Chiang'], + ['The Employees', 'Olga Ravn'], + ['Ancillary Justice', 'Ann Leckie'], + ['The Three-Body Problem', 'Liu Cixin'], + ['Hyperion', 'Dan Simmons'], + ['Blindsight', 'Peter Watts'], + ['The Sparrow', 'Mary Doria Russell'], + ['Void Star', 'Zachary Mason'], + ['Semiosis', 'Sue Burke'], + ['The Book of Strange New Things', 'Michel Faber'], + ['A Canticle for Leibowitz', 'Walter M. Miller Jr.'], + ['Gnomon', 'Nick Harkaway'], + ['The Peripheral', 'William Gibson'], + ['Version Control', 'Dexter Palmer'], +]; + module.exports = function booksDemo({ demo: { wave, round } }) { - return { - provider: 'audiobookshelf', - source: 'unread', - books: [ - { - title: 'The Left Hand of Darkness', - author: 'Ursula K. Le Guin', - progress: round(wave(900, 0.05, 0.95), 3), - finished: false, - color: null, - kind: 'book', - }, - { - title: 'Piranesi', - author: 'Susanna Clarke', - progress: round(wave(700, 0.05, 0.95, 1.7), 3), - finished: false, - color: null, - kind: 'book', - }, - { - title: 'The Dispossessed', - author: 'Ursula K. Le Guin', - progress: 1, - finished: true, - color: null, - kind: 'book', - }, - { - title: 'Klara and the Sun', - author: 'Kazuo Ishiguro', - progress: null, - finished: false, + const shelf = (source, offset) => ({ + source, + books: TITLES.slice(offset, offset + 16).map(([title, author], i) => { + const n = offset + i; + const finished = n % 5 === 2; + const unread = source === 'unread' || n % 5 === 3; + return { + title, + author, + progress: finished ? 1 : unread ? null : round(wave(700 + n * 60, 0.05, 0.95, 1.7 * n), 3), + finished, color: null, kind: 'book', - }, - ], + }; + }), + }); + return { + provider: 'audiobookshelf', + shelves: [shelf('unread', 0), shelf('recently', 8), shelf('list', 12)], }; }; diff --git a/public/widgets/books/i18n/de.json b/public/widgets/books/i18n/de.json index b48d400..fd5c70b 100644 --- a/public/widgets/books/i18n/de.json +++ b/public/widgets/books/i18n/de.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "API-Schlüssel", "kavitaKey.hint": "User Settings → 3rd Party Clients.", + "shelves.label": "Regal", "source.label": "Anzeigen", "source.opt.recently": "Zuletzt hinzugefügt", "source.opt.unread": "Ungelesen / Weiterlesen", diff --git a/public/widgets/books/i18n/en.json b/public/widgets/books/i18n/en.json index 12cfe9c..7609b63 100644 --- a/public/widgets/books/i18n/en.json +++ b/public/widgets/books/i18n/en.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "API key", "kavitaKey.hint": "User Settings → 3rd Party Clients.", + "shelves.label": "Shelf", "source.label": "Show", "source.opt.recently": "Recently Added", "source.opt.unread": "Unread / On-deck", diff --git a/public/widgets/books/i18n/es.json b/public/widgets/books/i18n/es.json index 0e0f24f..a8fe27e 100644 --- a/public/widgets/books/i18n/es.json +++ b/public/widgets/books/i18n/es.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "Clave de API", "kavitaKey.hint": "User Settings → 3rd Party Clients.", + "shelves.label": "Estante", "source.label": "Mostrar", "source.opt.recently": "Añadidos recientemente", "source.opt.unread": "Sin leer / En curso", diff --git a/public/widgets/books/i18n/fa.json b/public/widgets/books/i18n/fa.json index 108d625..b04877e 100644 --- a/public/widgets/books/i18n/fa.json +++ b/public/widgets/books/i18n/fa.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "کلید API", "kavitaKey.hint": "User Settings ← 3rd Party Clients.", + "shelves.label": "قفسه", "source.label": "نمایش", "source.opt.recently": "به‌تازگی افزوده‌شده", "source.opt.unread": "خوانده‌نشده / در دست خواندن", diff --git a/public/widgets/books/i18n/fr.json b/public/widgets/books/i18n/fr.json index 824f718..4aaf26d 100644 --- a/public/widgets/books/i18n/fr.json +++ b/public/widgets/books/i18n/fr.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "Clé d’API", "kavitaKey.hint": "User Settings → 3rd Party Clients.", + "shelves.label": "Étagère", "source.label": "Afficher", "source.opt.recently": "Ajouts récents", "source.opt.unread": "Non lus / En cours", diff --git a/public/widgets/books/i18n/zh-Hans.json b/public/widgets/books/i18n/zh-Hans.json index 0e2d316..eea12fa 100644 --- a/public/widgets/books/i18n/zh-Hans.json +++ b/public/widgets/books/i18n/zh-Hans.json @@ -16,6 +16,7 @@ "kavitaUrl.placeholder": "http://host:5000", "kavitaKey.label": "API 密钥", "kavitaKey.hint": "User Settings → 3rd Party Clients。", + "shelves.label": "书架", "source.label": "显示", "source.opt.recently": "最近添加", "source.opt.unread": "未读 / 继续阅读", diff --git a/public/widgets/books/index.html b/public/widgets/books/index.html index 283f674..e8d36e7 100644 --- a/public/widgets/books/index.html +++ b/public/widgets/books/index.html @@ -20,7 +20,13 @@ white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .head svg{width:13px;height:13px;color:#636366;flex:none} + .shelves{flex:1;display:flex;flex-direction:column;min-height:0} + .shelf-wrap{flex:1;display:flex;flex-direction:column;min-height:0} + .shelf-lbl{flex:none;font-size:9px;font-weight:600;color:#636366;letter-spacing:.06em;margin-top:5px; + white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .shelf-lbl[hidden]{display:none} .shelf{flex:1;position:relative;padding-bottom:9px;margin-top:6px;perspective:520px} + .size-large .shelf{margin-top:2px} .books{list-style:none;height:100%;display:flex;align-items:flex-end;justify-content:center;gap:2px} .empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; color:#636366;font-size:11px} @@ -75,9 +81,14 @@ -
- - +
+
+ +
+
    + +
    +