From 42e7287389ddd6e8da4bb7085c31624fa9c5fc5b Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Sat, 3 Oct 2026 07:25:59 +0200 Subject: [PATCH] Fold each group of settings into an open card The long help for the download template and the date format now folds shut. --- bindery/static/js/rules.js | 6 +- bindery/static/js/settings.js | 164 +++++++++++++++++++--------------- bindery/static/style.css | 10 +++ tests/js/flows.test.mjs | 21 ++++- 4 files changed, 126 insertions(+), 75 deletions(-) diff --git a/bindery/static/js/rules.js b/bindery/static/js/rules.js index 4667dac..032cbad 100644 --- a/bindery/static/js/rules.js +++ b/bindery/static/js/rules.js @@ -227,9 +227,9 @@ export function globalRulesCard() { .catch(showError); return h( - 'section', - { class: 'card' }, - h('h2', {}, 'Global replacement rules'), + 'details', + { class: 'card', open: true }, + h('summary', {}, h('h2', {}, 'Global replacement rules')), h( 'p', { class: 'hint' }, diff --git a/bindery/static/js/settings.js b/bindery/static/js/settings.js index afcc51b..82e0556 100644 --- a/bindery/static/js/settings.js +++ b/bindery/static/js/settings.js @@ -9,6 +9,9 @@ const DEFAULTS_INI = 'https://github.com/JimmXinu/FanFicFare/blob/main/fanficfar const TEMPLATE_DOCS = 'https://manual.calibre-ebook.com/template_lang.html'; const DATE_CODES = 'https://docs.python.org/3/library/datetime.html#format-codes'; const code = (text) => h('code', {}, text); +// A card of settings that folds shut. It starts open. +const card = (title, ...children) => + h('details', { class: 'card', open: true }, h('summary', {}, h('h2', {}, title)), ...children); export function settingsPage(main, _params, config) { document.title = 'Settings ยท Bindery'; @@ -104,11 +107,9 @@ export function settingsPage(main, _params, config) { fill( main, - h( - 'section', - { class: 'card' }, - h('h1', {}, 'Settings'), - h('h2', {}, 'Recheck recent chapters'), + h('h1', {}, 'Settings'), + card( + 'Recheck recent chapters', h( 'div', { class: 'controls' }, @@ -124,10 +125,8 @@ export function settingsPage(main, _params, config) { ), saveButton(() => ({ recent_chapters: recent.value === '' ? 0 : Number(recent.value) })), ), - config.cwa_enabled && h( - 'section', - { class: 'card' }, - h('h2', {}, 'Uploads to CWA'), + config.cwa_enabled && card( + 'Uploads to CWA', h('label', {}, linkBooks, ' Link books to CWA library'), h( 'p', @@ -161,73 +160,100 @@ export function settingsPage(main, _params, config) { }, ), ), - h( - 'section', - { class: 'card' }, - h('h2', {}, 'Book Downloads'), + card( + 'Book Downloads', h('div', { class: 'controls' }, h('label', { for: 'download-template' }, 'File name template'), template), h( 'p', { class: 'hint' }, - 'This template gives the file name of a book that you download from a story page. It follows the ', - externalLink(TEMPLATE_DOCS, 'template rules of Calibre'), - ' for file names. Bindery replaces each field in braces with the value of the book, and adds .epub. ' + - 'A field with no value becomes empty text. To go back to the default, {title} - {authors}, clear the field ' + - 'and save. ' + - 'For example, ', - code('{series:|| - }{series_index:0>2s|| - }{title} - {authors}'), - ' gives "Saga, The - 02 - Book, The - Ann Writer.epub".', + 'This template gives the file name of a book that you download from a story page. To go back to the ' + + 'default, ', + code('{title} - {authors}'), + ', clear the field and save.', + ), + h( + 'details', + { class: 'help' }, + h('summary', {}, 'File name template help'), + h( + 'p', + { class: 'hint' }, + 'The template follows the ', + externalLink(TEMPLATE_DOCS, 'template rules of Calibre'), + ' for file names. Bindery replaces each field in braces with the value of the book, and adds .epub. ' + + 'A field with no value becomes empty text. For example, ', + code('{series:|| - }{series_index:0>2s|| - }{title} - {authors}'), + ' gives "Saga, The - 02 - Book, The - Ann Writer.epub".', + ), + h('p', { class: 'hint' }, 'You can use these fields:'), + h( + 'dl', + { class: 'fields' }, + Object.entries(config.download_fields).map(([name, text]) => [h('dt', {}, code(`{${name}}`)), h('dd', {}, text)]), + ), + h( + 'ul', + { class: 'hint' }, + h('li', {}, code('{title:.20s}'), ' gives the first 20 characters of the title.'), + h('li', {}, code('{author_sort[0]}'), ' gives the first character of the author sort string.'), + h( + 'li', + {}, + code('{series:|| - }'), + ' adds " - " after the series, but only for a book with a series. The form is ', + code('{field:format|text before|text after}'), + '.', + ), + h('li', {}, code('{{'), ' and ', code('}}'), ' give braces.'), + h( + 'li', + {}, + 'The title and the series move a leading "The", "A" or "An" to the end, as Calibre does. ' + + '"The Book" becomes "Book, The".', + ), + h( + 'li', + {}, + 'A download has one file name. If the template has folders, Bindery uses the part after the last ', + code('/'), + '.', + ), + h( + 'li', + {}, + 'Bindery does not support template functions, such as ', + code('{title:uppercase()}'), + ', or the program modes of Calibre.', + ), + h('li', {}, 'If the template fails or gives no name for a book, Bindery names the file after the title.'), + ), ), h('div', { class: 'controls' }, h('label', { for: 'download-date-format' }, 'Date format'), dateFormat), h( 'p', { class: 'hint' }, - 'The form of the dates in {pubdate}, {timestamp} and {last_modified}. %d gives the day, %b the month name, ' + - '%m the month number and %Y the year. See the ', - externalLink(DATE_CODES, 'list of all codes'), - '. To go back to the default, %b, %Y, clear the field and save. The default gives "May, 2020".', - ), - h('p', { class: 'hint' }, 'You can use these fields:'), - h( - 'dl', - { class: 'fields' }, - Object.entries(config.download_fields).map(([name, text]) => [h('dt', {}, code(`{${name}}`)), h('dd', {}, text)]), + 'The form of the dates in {pubdate}, {timestamp} and {last_modified}. To go back to the default, ', + code('%b, %Y'), + ', clear the field and save.', ), h( - 'ul', - { class: 'hint' }, - h('li', {}, code('{title:.20s}'), ' gives the first 20 characters of the title.'), - h('li', {}, code('{author_sort[0]}'), ' gives the first character of the author sort string.'), + 'details', + { class: 'help' }, + h('summary', {}, 'Date format help'), h( - 'li', - {}, - code('{series:|| - }'), - ' adds " - " after the series, but only for a book with a series. The form is ', - code('{field:format|text before|text after}'), - '.', + 'p', + { class: 'hint' }, + code('%d'), + ' gives the day, ', + code('%b'), + ' the month name, ', + code('%m'), + ' the month number and ', + code('%Y'), + ' the year. See the ', + externalLink(DATE_CODES, 'list of all codes'), + '. The default gives "May, 2020". A / in a date becomes _, as in Calibre.', ), - h('li', {}, code('{{'), ' and ', code('}}'), ' give braces.'), - h( - 'li', - {}, - 'The title and the series move a leading "The", "A" or "An" to the end, as Calibre does. ' + - '"The Book" becomes "Book, The".', - ), - h( - 'li', - {}, - 'A download has one file name. If the template has folders, Bindery uses the part after the last ', - code('/'), - '.', - ), - h( - 'li', - {}, - 'Bindery does not support template functions, such as ', - code('{title:uppercase()}'), - ', or the program modes of Calibre.', - ), - h('li', {}, 'If the template fails or gives no name for a book, Bindery names the file after the title.'), ), saveButton( () => ({ download_template: template.value, download_date_format: dateFormat.value }), @@ -238,10 +264,8 @@ export function settingsPage(main, _params, config) { }, ), ), - h( - 'section', - { class: 'card' }, - h('h2', {}, 'Jobs list'), + card( + 'Jobs list', h( 'div', { class: 'controls' }, @@ -274,10 +298,8 @@ export function settingsPage(main, _params, config) { })), ), globalRulesCard(), - h( - 'section', - { class: 'card' }, - h('h2', {}, 'FanFicFare settings (personal.ini)'), + card( + 'FanFicFare settings (personal.ini)', h( 'p', { class: 'hint' }, diff --git a/bindery/static/style.css b/bindery/static/style.css index 65215ef..5b26594 100644 --- a/bindery/static/style.css +++ b/bindery/static/style.css @@ -1063,6 +1063,16 @@ textarea { margin-top: 12px; } +/* Help that folds shut under a setting, such as the help for the download template. */ +.help { + margin: 0 0 12px; + + > summary { + color: var(--muted); + font-size: var(--text-sm); + } +} + /* Toasts */ #toasts { diff --git a/tests/js/flows.test.mjs b/tests/js/flows.test.mjs index 91124f2..9202f70 100644 --- a/tests/js/flows.test.mjs +++ b/tests/js/flows.test.mjs @@ -55,7 +55,7 @@ for (const value of ['7', '']) { await settle(); api.on('PUT', 'settings', {}); field(main, 'recent-chapters').value = value; - await click(section(main, 'Settings'), 'Save'); + await click(section(main, 'Recheck recent chapters'), 'Save'); assert.deepEqual(api.sent('PUT', 'settings')[0].body, { recent_chapters: value === '' ? 0 : 7 }); assert.match(ui.toasts.textContent, /Saved/); }); @@ -109,6 +109,25 @@ test('the jobs list settings load and save all four fields', async () => { assert.match(ui.toasts.textContent, /Saved/); }); +test('each group of settings is an open card, and the download help starts closed', async () => { + settingsPage(main, {}, config); + await settle(); + const cards = main.children.filter((el) => el.tagName !== 'h1'); + assert.deepEqual(cards.map((el) => [el.tagName, el.className, el.open, el.children[0].textContent]), [ + ['details', 'card', true, 'Recheck recent chapters'], + ['details', 'card', true, 'Uploads to CWA'], + ['details', 'card', true, 'Book Downloads'], + ['details', 'card', true, 'Jobs list'], + ['details', 'card', true, 'Global replacement rules'], + ['details', 'card', true, 'FanFicFare settings (personal.ini)'], + ]); + const help = nodes(section(main, 'Book Downloads')).filter((el) => el.className === 'help'); + assert.deepEqual(help.map((el) => [el.tagName, el.open, el.children[0].textContent]), [ + ['details', undefined, 'File name template help'], + ['details', undefined, 'Date format help'], + ]); +}); + test('the download template loads, lists its fields, and saves the stored form', async () => { api.on('GET', 'settings', { recent_chapters: 0, personal_ini: '', download_template: '{title}', download_date_format: '%b, %Y',