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 @@ -
- - +
+
+ +
+
    + +
    +