Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=4c1189b9';
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=e8b2a9f7';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconName } from '/js/icons.js?v=69c2b9bd';
import { state } from '/js/admin-state.js?v=7d68e98e';
Expand All @@ -19,9 +19,9 @@ import {
initInlineEdit,
setTogDisabled,
wireChecklist,
} from '/js/admin-shared.js?v=d7dd8b71';
} from '/js/admin-shared.js?v=70ec6be1';
import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=f638b750';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=2d28867b';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';

export function buildFolderForm(body, item) {
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ag, ap } from '/js/admin-shared.js?v=d7dd8b71';
import { ag, ap } from '/js/admin-shared.js?v=70ec6be1';
import { t } from '/js/i18n.js?v=e644a5c5';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { el, inp as inpById, qa } from '/js/utils.js?v=4c1189b9';
import { el, inp as inpById, qa } from '/js/utils.js?v=e8b2a9f7';

export async function checkAuth(onLogin) {
try {
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=d7dd8b71';
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=70ec6be1';
import { t } from '/js/i18n.js?v=e644a5c5';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=4c1189b9';
import { qa, q } from '/js/utils.js?v=e8b2a9f7';

const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=7d68e98e';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { qa } from '/js/utils.js?v=4c1189b9';
import { qa } from '/js/utils.js?v=e8b2a9f7';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
8 changes: 4 additions & 4 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { reorderItems } from '/js/admin-logic.js?v=dcf7c37d';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=971d097b';
import { paintIcon } from '/js/admin-shared.js?v=d7dd8b71';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=4c1189b9';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=f3b01482';
import { paintIcon } from '/js/admin-shared.js?v=70ec6be1';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=e8b2a9f7';
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=cc1dfdec';
import { sizeLabel } from '/js/admin-widget-form.js?v=997ca7c6';
import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947';

/** @type {{ openModal: (idx: number|null) => void,
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { toast, ag, ap } from '/js/admin-shared.js?v=d7dd8b71';
import { toast, ag, ap } from '/js/admin-shared.js?v=70ec6be1';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { t } from '/js/i18n.js?v=e644a5c5';
import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=dcf7c37d';
import { confirmText } from '/js/modal.js?v=11fa1eff';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=4c1189b9';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=e8b2a9f7';

/* Mirrors the server's rule: auth cannot be switched on with no password. */
let _passwordSet = false;
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-shared.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Stateless helpers shared by the admin modules. Mutable state stays out. */
import { nextActiveIndex, recoversSession, toastHoldMs } from '/js/admin-logic.js?v=dcf7c37d';
import { el, qa, q } from '/js/utils.js?v=4c1189b9';
import { el, qa, q } from '/js/utils.js?v=e8b2a9f7';
import { t } from '/js/i18n.js?v=e644a5c5';
import { iconChain } from '/js/icons.js?v=69c2b9bd';

Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-widget-form.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { state } from '/js/admin-state.js?v=7d68e98e';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=d7dd8b71';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=02918336';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=70ec6be1';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=e6ec5157';
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';
import { q, qi, qa } from '/js/utils.js?v=4c1189b9';
import { q, qi, qa } from '/js/utils.js?v=e8b2a9f7';

const SIZE_ICONS = {
small:
Expand Down
19 changes: 10 additions & 9 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=4a5478d6';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=681cfd29';
import { initList, render, syncFilterUI } from '/js/admin-list.js?v=8524801f';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=8c9ef961';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=1bb8584a';
import { initList, render, syncFilterUI } from '/js/admin-list.js?v=2067483a';
import { resolveAdminSection } from '/js/admin-logic.js?v=dcf7c37d';
import {
buildAppItem,
Expand All @@ -10,10 +10,10 @@ import {
snapshotItems,
upsertItem,
} from '/js/admin-save-logic.js?v=4f71ef6c';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=909c58ad';
import { ag, ap, initInlineEdit, paintIcon, setReauthHandler, toast } from '/js/admin-shared.js?v=d7dd8b71';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=9f7cf590';
import { ag, ap, initInlineEdit, paintIcon, setReauthHandler, toast } from '/js/admin-shared.js?v=70ec6be1';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=cc1dfdec';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=997ca7c6';
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';
Expand All @@ -29,8 +29,8 @@ import {
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=9de1cb7d';
import { confirmModal, confirmText, openModal as openDialog, promptModal } from '/js/modal.js?v=11fa1eff';
import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=00c011c9';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=4c1189b9';
import { normalizeColorInput } from '/js/admin-color-control.js?v=f638b750';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=e8b2a9f7';
import { normalizeColorInput } from '/js/admin-color-control.js?v=2d28867b';
import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=6ebb564c';
import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6';

Expand Down Expand Up @@ -609,7 +609,8 @@ function initNav() {
localStorage.setItem(STORE, id);
}
links.forEach(l => l.addEventListener('click', () => show(l.dataset.sec)));
show(localStorage.getItem(STORE));
addEventListener('hashchange', () => show(location.hash.slice(1)));
show(location.hash.slice(1) || localStorage.getItem(STORE));
}

function initAllInlineEdits() {
Expand Down
6 changes: 3 additions & 3 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import {
setUserText,
teardownWidgets,
titleWhenTruncated,
} from '/js/utils.js?v=4c1189b9';
import { initSpotlight } from '/js/spotlight.js?v=c8d5d2d8';
} from '/js/utils.js?v=e8b2a9f7';
import { initSpotlight } from '/js/spotlight.js?v=c81114db';
import { html, setHtml, raw } from '/js/html.js?v=c71f8903';
import { initI18n, t, currentLang } from '/js/i18n.js?v=e644a5c5';
import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7';
import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f';
import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=b71e1c75';
import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=3c2c7d0b';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=b3c8b6c2';
import { formatNumber } from '/js/format-number.js?v=4a5ccef4';
import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=aa52b1a3';
Expand Down
2 changes: 1 addition & 1 deletion public/js/spotlight.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=4c1189b9';
import { mk, clr, el, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=e8b2a9f7';
import { t } from '/js/i18n.js?v=e644a5c5';

/* Attached to the window so a re-open can undo the previous one. */
Expand Down
2 changes: 1 addition & 1 deletion public/js/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
q,
qa,
setUserText,
} from '/js/utils.js?v=4c1189b9';
} from '/js/utils.js?v=e8b2a9f7';
import { t, currentLang } from '/js/i18n.js?v=e644a5c5';
import { toneForColor } from '/js/label-contrast.js?v=c1ac6fb8';
import { mobileMetrics, gridColumnWidth, gridCellCount } from '/js/mobile-metrics.js?v=ab5fe77e';
Expand Down
4 changes: 2 additions & 2 deletions public/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ export function renderEmptyState(items) {
box.className = 'empty-state';
const mark = mk('img', { src: BRAND_MARK, alt: '' });
mark.className = 'empty-state-mark';
const title = mk('p');
const title = mk('h2');
title.className = 'empty-state-title';
title.textContent = t('home.emptyTitle');
const sub = mk('p');
Expand All @@ -426,7 +426,7 @@ export function renderEmptyState(items) {
const btn = mk('a', { href: '/admin/' });
btn.className = 'empty-state-btn';
btn.textContent = t('home.emptyAction');
const hint = mk('a', { href: '/admin/' });
const hint = mk('a', { href: '/admin/#general' });
hint.className = 'empty-state-hint';
hint.textContent = t('home.emptyImportLink');
box.append(mark, title, sub, btn, hint);
Expand Down
6 changes: 3 additions & 3 deletions public/js/widget-config-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import { t } from '/js/i18n.js?v=e644a5c5';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { wireChecklist } from '/js/admin-shared.js?v=d7dd8b71';
import { renderColorControl } from '/js/admin-color-control.js?v=f638b750';
import { wireChecklist } from '/js/admin-shared.js?v=70ec6be1';
import { renderColorControl } from '/js/admin-color-control.js?v=2d28867b';
import {
seedCarried,
applyOptionSet,
Expand All @@ -15,7 +15,7 @@ import {
visibleFieldFlags,
} from '/js/admin-logic.js?v=dcf7c37d';
import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';
import { qi } from '/js/utils.js?v=4c1189b9';
import { qi } from '/js/utils.js?v=e8b2a9f7';

const PE =
'<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/><path d="M18.4 2.6a1.85 1.85 0 0 1 2.6 2.6l-9.1 9.1-3.4 1 1-3.4z"/></svg>';
Expand Down
15 changes: 12 additions & 3 deletions public/js/widget-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ const CSS = `
its own hidden rule the caption never goes away. */
.wt-cap[hidden] { display: none; }
.wt-cap svg { width: 12px; height: 12px; flex: 0 0 auto; opacity: 0.85; }
.wt-cap b { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wt-cap b { font-weight: 500; overflow: hidden; min-width: 0;
/* Two lines, then clip. The narrowest card cannot hold the longest line on
one, and an ellipsised failure does not say what failed. */
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.wt-cap i { font-style: normal; opacity: 0.7; flex: 0 0 auto; }
.wt-cap-auto { position: absolute; inset-inline: 16px; bottom: 12px; }
.wt-cap-center { position: absolute; inset: 0; justify-content: center; text-align: center; padding: 0 14px; }
Expand All @@ -129,8 +132,14 @@ function ensureStyle(doc) {
@param {any} opts */
export function errorState(opts = {}) {
const root = opts.root;
const contentOf = () =>
[].concat((typeof opts.content === 'function' ? opts.content() : opts.content) || root || []).filter(Boolean);
/* Named content, else everything in the root but the caption. inert is a CSS
filter, which a descendant cannot escape, so fading the root would fade the
failure line with it. */
const contentOf = () => {
const named = typeof opts.content === 'function' ? opts.content() : opts.content;
if (named) return [].concat(named).filter(Boolean);
return root ? [...root.children].filter(el => el !== cap) : [];
};
const t = typeof opts.t === 'function' ? opts.t : (_k, fallback) => fallback;
const doc = root && root.ownerDocument;
ensureStyle(doc);
Expand Down
2 changes: 1 addition & 1 deletion public/js/widget-toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { esc, html, setHtml } from '/js/html.js?v=c71f8903';
import { isSafeLinkUrl } from '/js/link-url.js?v=54adb40f';
import { jitter } from '/js/jitter.js?v=4eeef4c9';
import { errorState as _errorState, errorKind, errorCopy } from '/js/widget-error.js?v=e06eef64';
import { errorState as _errorState, errorKind, errorCopy } from '/js/widget-error.js?v=27d63b49';

export { esc, html, setHtml };

Expand Down
2 changes: 1 addition & 1 deletion public/widgets/backup/backup.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
<body>
<div class="widget" id="widget" role="group" aria-label="Backup status"></div>
<script type="module">
import { poll, fetchData, openUrl, html, setHtml, loadStrings, wt, errorState } from '/js/widget-toolbox.js?v=c27f1c81';
import { poll, fetchData, openUrl, html, setHtml, loadStrings, wt, errorState } from '/js/widget-toolbox.js?v=833f5b5f';
await loadStrings();
document.getElementById('widget').setAttribute('aria-label', wt('ui.backupStatus', 'Backup status'));
const id = new URLSearchParams(location.search).get('id') || '';
Expand Down
42 changes: 36 additions & 6 deletions public/widgets/books/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ function jpost(ctx, base, path, headers, body) {
function authErr(r) {
return r.status === 401 || r.status === 403;
}
/* The reader's own word for a shelf, when the service has one. */
function listName(r) {
const d = r && r.data;
return (d && typeof d.name === 'string' && d.name) || '';
}
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 }));
return list.map(r => ({
source: (r && r.source) || 'recently',
listId: r && r.listId,
label: r && typeof r.label === 'string' ? r.label.trim() : '',
}));
}

/* ───────────────────────── Audiobookshelf ───────────────────────── */
Expand Down Expand Up @@ -52,10 +61,10 @@ async function absItems(ctx, base, hdr, lib, shelf) {
const [kind, id] = String(shelf.listId).split(':');
if (kind === 'playlist') {
const r = await jget(ctx, base, `/api/playlists/${enc(id)}`, hdr);
return ((r.data && r.data.items) || []).map(it => it.libraryItem || it);
return { name: listName(r), items: ((r.data && r.data.items) || []).map(it => it.libraryItem || it) };
}
const r = await jget(ctx, base, `/api/collections/${enc(id)}`, hdr);
return (r.data && r.data.books) || [];
return { name: listName(r), items: (r.data && r.data.books) || [] };
}
if (shelf.source === 'unread') {
const f = Buffer.from('not-finished').toString('base64');
Expand All @@ -65,10 +74,10 @@ async function absItems(ctx, base, hdr, lib, shelf) {
`/api/libraries/${enc(lib)}/items?filter=progress.${f}&sort=addedAt&desc=1&limit=${CAP}`,
hdr,
);
return (r.data && r.data.results) || [];
return { name: '', items: (r.data && r.data.results) || [] };
}
const r = await jget(ctx, base, `/api/libraries/${enc(lib)}/items?sort=addedAt&desc=1&limit=${CAP}`, hdr);
return (r.data && r.data.results) || [];
return { name: '', items: (r.data && r.data.results) || [] };
}
async function abs(ctx) {
const base = ctx.normalizeBase(ctx.config.absUrl);
Expand All @@ -78,9 +87,10 @@ async function abs(ctx) {
const lib = await absLibrary(ctx, base, hdr);
const shelves = [];
for (const shelf of shelvesOf(ctx)) {
const items = await absItems(ctx, base, hdr, lib, shelf);
const { name, items } = await absItems(ctx, base, hdr, lib, shelf);
shelves.push({
source: shelf.source,
name: shelf.label || name,
books: cap(items)
.map(absBook)
.filter(b => b.title),
Expand Down Expand Up @@ -122,6 +132,13 @@ function komgaBook(b) {
kind: 'book',
};
}
/* The books endpoint carries no list name, so the same listing the settings
picker uses is read back. Only for a shelf the reader has not named. */
async function komgaListName(ctx, base, hdr, listId) {
const r = await jget(ctx, base, `/api/v1/readlists?size=100`, hdr);
const hit = ((r.data && r.data.content) || []).find(l => l && String(l.id) === String(listId));
return (hit && hit.name) || '';
}
async function komga(ctx) {
const base = ctx.normalizeBase(ctx.config.komgaUrl);
const key = ctx.config.komgaKey;
Expand All @@ -136,8 +153,11 @@ async function komga(ctx) {
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 : []);
let name = shelf.label;
if (!name && shelf.source === 'list' && shelf.listId) name = await komgaListName(ctx, base, hdr, shelf.listId);
shelves.push({
source: shelf.source,
name,
books: cap(content)
.map(komgaBook)
.filter(b => b.title),
Expand Down Expand Up @@ -194,6 +214,13 @@ async function kavitaList(ctx, base, hdr, shelf) {
});
return Array.isArray(r.data) ? r.data : [];
}
/* Same as Komga: the items endpoint carries no list title. */
async function kavitaListName(ctx, base, hdr, listId) {
const r = await jpost(ctx, base, `/api/ReadingList/lists?PageNumber=1&PageSize=100&includePromoted=true`, hdr);
const arr = Array.isArray(r.data) ? r.data : (r.data && r.data.content) || [];
const hit = arr.find(l => l && String(l.id) === String(listId));
return (hit && (hit.title || hit.name)) || '';
}
async function kavita(ctx) {
const base = ctx.normalizeBase(ctx.config.kavitaUrl);
const key = ctx.config.kavitaKey;
Expand All @@ -203,8 +230,11 @@ async function kavita(ctx) {
const shelves = [];
for (const shelf of shelvesOf(ctx)) {
const list = await kavitaList(ctx, base, hdr, shelf);
let name = shelf.label;
if (!name && shelf.source === 'list' && shelf.listId) name = await kavitaListName(ctx, base, hdr, shelf.listId);
shelves.push({
source: shelf.source,
name,
books: cap(list)
.map(kavitaSeries)
.filter(b => b.title),
Expand Down
Loading