From c1d44c25b4beb53f0ef63db2cad43660f7feb692 Mon Sep 17 00:00:00 2001 From: codebude Date: Fri, 11 Sep 2026 13:56:17 +0200 Subject: [PATCH 1/3] Added basket functionality --- frontend/package-lock.json | 15 ++ frontend/package.json | 1 + .../src/lib/components/AddBookModal.svelte | 100 +++++++++- .../src/lib/components/AddBookModal.test.ts | 172 +++++++++++++++++- .../src/lib/components/BasketPanel.svelte | 103 +++++++++++ .../src/lib/components/ImportSearch.svelte | 37 +++- .../src/lib/components/ImportSearch.test.ts | 170 ++++++++++++++++- frontend/src/lib/i18n/locales/de.json | 13 +- frontend/src/lib/i18n/locales/en.json | 13 +- frontend/src/lib/i18n/locales/es.json | 13 +- frontend/src/lib/i18n/locales/fr.json | 13 +- frontend/src/lib/i18n/locales/zh.json | 13 +- frontend/src/lib/types.ts | 8 + frontend/src/lib/utils/importSearch.test.ts | 26 +++ frontend/src/lib/utils/importSearch.ts | 7 + 15 files changed, 686 insertions(+), 18 deletions(-) create mode 100644 frontend/src/lib/components/BasketPanel.svelte diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ecb65371..2a7e478b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^7.0.1", "@testing-library/svelte": "^5.3.1", + "@testing-library/user-event": "^14.6.7", "@types/hammerjs": "^2.0.46", "@types/node": "^26.2.0", "@vitest/coverage-v8": "^4.1.7", @@ -1033,6 +1034,20 @@ "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 16116bc9..27ad7106 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^7.0.1", "@testing-library/svelte": "^5.3.1", + "@testing-library/user-event": "^14.6.7", "@types/hammerjs": "^2.0.46", "@types/node": "^26.2.0", "@vitest/coverage-v8": "^4.1.7", diff --git a/frontend/src/lib/components/AddBookModal.svelte b/frontend/src/lib/components/AddBookModal.svelte index 5493de08..7314431f 100644 --- a/frontend/src/lib/components/AddBookModal.svelte +++ b/frontend/src/lib/components/AddBookModal.svelte @@ -1,14 +1,15 @@ + +
+ {#if basket.length === 0} +
+ +

{$_('import.basketEmpty')}

+
+ {:else} + +
+ + {$_('import.basketCount', { values: { count: basket.length } })} + + +
+ {/if} +
\ No newline at end of file diff --git a/frontend/src/lib/components/ImportSearch.svelte b/frontend/src/lib/components/ImportSearch.svelte index 9f3d37d1..fffbf97c 100644 --- a/frontend/src/lib/components/ImportSearch.svelte +++ b/frontend/src/lib/components/ImportSearch.svelte @@ -1,5 +1,5 @@
@@ -399,6 +423,7 @@ {@const selected = selectedCandidate(group)} {@const groupImported = isGroupAlreadyImported(group)} {@const importingKey = `${group.key}:${group.variants.indexOf(selected)}`} + {@const inBasket = isInBasket(selected)}
  • {#if group.coverUrl} @@ -444,7 +469,7 @@ class="btn btn-xs {groupImported ? 'btn-success btn-outline' : 'btn-primary'}" disabled={groupImported || importing === importingKey || !acquisitionStatus} title={groupImported ? $_('import.alreadyImported') : ''} - onclick={() => importBook(group, 'want_to_read')} + onclick={() => importBook(group, defaultStatus)} > {importing === importingKey ? $_('common.loadingEllipsis') @@ -452,6 +477,14 @@ ? $_('import.imported') : $_('app.add')} + {#if group.variants.length > 1}
    + {#if isSelected} + ✓ {$_('import.editionSelected')} + {/if} {#if variantImported} {$_('import.alreadyImported')} {/if} diff --git a/frontend/src/lib/components/ImportSearch.test.ts b/frontend/src/lib/components/ImportSearch.test.ts index b7dcba61..6a4f7c9e 100644 --- a/frontend/src/lib/components/ImportSearch.test.ts +++ b/frontend/src/lib/components/ImportSearch.test.ts @@ -532,7 +532,11 @@ describe('ImportSearch', () => { await waitFor(() => { expect(screen.getByRole('button', { name: /^hardcover/ })).toBeInTheDocument(); }); - await fireEvent.click(screen.getByRole('button', { name: /^hardcover/ })); + const hardcoverButton = screen.getByRole('button', { name: /^hardcover/ }); + await fireEvent.click(hardcoverButton); + expect(hardcoverButton).toHaveAttribute('aria-pressed', 'true'); + expect(hardcoverButton).toHaveClass('border-primary', 'bg-primary/10', 'cursor-pointer'); + expect(hardcoverButton).toHaveTextContent('Selected'); // Google supplement appends a third variant. await fireEvent.click(screen.getByRole('button', { name: 'Search Google Books too' })); diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 6835fc1c..91b12dd2 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -254,6 +254,7 @@ "groupExpand": "Ausgaben anzeigen", "groupCollapse": "Ausgaben ausblenden", "groupChooseEdition": "Ausgabe wählen", + "editionSelected": "Ausgewählt", "addToBasket": "Zum Warenkorb hinzufügen", "inBasket": "Im Warenkorb", "basketEmpty": "Dein Warenkorb ist leer. Suche nach Büchern, um sie hinzuzufügen.", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 5e87a6b2..bdd88a9b 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -254,6 +254,7 @@ "groupExpand": "Show editions", "groupCollapse": "Hide editions", "groupChooseEdition": "Choose edition", + "editionSelected": "Selected", "addToBasket": "Add to Basket", "inBasket": "In basket", "basketEmpty": "Your basket is empty. Search for books to add.", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index 55cba34c..8d74fe30 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -254,6 +254,7 @@ "groupExpand": "Mostrar ediciones", "groupCollapse": "Ocultar ediciones", "groupChooseEdition": "Elegir edición", + "editionSelected": "Seleccionada", "addToBasket": "Añadir a la cesta", "inBasket": "En la cesta", "basketEmpty": "Tu cesta está vacía. Busca libros para añadirlos.", diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index 9f17f4f1..cdb6a4ef 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -254,6 +254,7 @@ "groupExpand": "Afficher les éditions", "groupCollapse": "Masquer les éditions", "groupChooseEdition": "Choisir l'édition", + "editionSelected": "Sélectionnée", "addToBasket": "Ajouter au panier", "inBasket": "Dans le panier", "basketEmpty": "Votre panier est vide. Recherchez des livres à ajouter.", diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index d01629a7..81980c4f 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -254,6 +254,7 @@ "groupExpand": "显示版本", "groupCollapse": "隐藏版本", "groupChooseEdition": "选择版本", + "editionSelected": "已选择", "addToBasket": "添加到购物篮", "inBasket": "已在购物篮", "basketEmpty": "购物篮为空。搜索图书以添加。", From c26e235786e1c4fa65b2b163090418936615dd3c Mon Sep 17 00:00:00 2001 From: codebude Date: Fri, 11 Sep 2026 14:41:32 +0200 Subject: [PATCH 3/3] Document import basket and selected edition highlight in release notes --- docs/releases.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 1aa433e9..2079cac6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -39,10 +39,10 @@ LibrisLog v1.8.0 brings camera selection and zoom control to the barcode scanner -**Summary:** Adds shareable read-only public profile pages with configurable access and content, groups duplicate import-search results into expandable edition groups, makes running searches cancelable, introduces an adaptive date input with a native picker, adds optional book media and medium statistics, supports localized medium and possession searches, detects insecure camera contexts, and fixes timezone handling in the daily page statistics and progress log editing. +**Summary:** Adds shareable read-only public profile pages with configurable access and content, groups duplicate import-search results into expandable edition groups, lets you collect search results in an import basket and import them all at once, makes running searches cancelable, introduces an adaptive date input with a native picker, adds optional book media and medium statistics, supports localized medium and possession searches, detects insecure camera contexts, and fixes timezone handling in the daily page statistics and progress log editing. **Features** -- 📚 **Edition groups in the import search**: results from different providers that describe the same book (same ISBN, or same title and authors) are now grouped into expandable entries with an "N results" badge. Compare the variants side by side and import the one you want; no result is dropped anymore. See the [Library guide](/guide/using-librislog/library#how-results-are-grouped) for the exact grouping rules +- 📚 **Edition groups in the import search**: results from different providers that describe the same book (same ISBN, or same title and authors) are now grouped into expandable entries with an "N results" badge. Compare the variants side by side and import the one you want; no result is dropped anymore. The selected edition is highlighted with a border and a "Selected" badge, and every edition row shows a pointer cursor, hover feedback, and a keyboard focus ring. See the [Library guide](/guide/using-librislog/library#how-results-are-grouped) for the exact grouping rules - 🗂️ **Optional book medium**: classify books as Print, eBook, Audiobook, Comic / Graphic Novel, or Magazine / Newspaper from manual entry, search import, and book editing. Mediums can be filtered in the library, searched with `medium:`, imported/exported, and reviewed in the statistics distribution - 🌍 **Localized search values**: `medium:` and `possession:` searches accept both their original enum keys and localized display values, such as `medium:Hörbuch` and `possession:Im Besitz` - 🛑 **Cancelable book search**: while an import search is running, the Search button becomes a Cancel button, so you can stop the request and refine your query @@ -53,6 +53,7 @@ LibrisLog v1.8.0 brings camera selection and zoom control to the barcode scanner - 🎥 **Active camera name in the scanner**: the barcode scanner now shows the name of the active camera in a badge next to the switch button, so you always know which lens is being used - 🔗 **Heimdall dashboard integration**: new documentation for the LibrisLog enhanced app, which shows your reading statistics directly on [Heimdall](https://github.com/linuxserver/Heimdall) tiles - 🔗 **Shareable public profile pages**: create named, read-only profile URLs from the Profile page. Configure each link independently for public or logged-in-only access, selected profile sections and statistics, language, and an optional expiration date. Shared pages include responsive book cards, a mobile-safe reading timeline with incremental loading and hidden-book hints, full-library search with incremental loading, selectable 12-month/3-year/all-time trend ranges with value tooltips, distribution and rating panels, and the owner's generated avatar. Existing links can be copied, opened, edited, or revoked. The full URL token is only revealed on demand and is shown once after creation. See the [Profile guide](/guide/using-librislog/profile#urlprofile-sharing) for setup and security details +- 🧺 **Import basket**: search results now offer an **Add to Basket** action next to the existing **Add** button. Collected books appear in a new **Basket** tab with a live count badge, where you can review them, remove individual entries, and import everything in one go. Each entry remembers the reading status, possession status, and medium that were selected when it was added. If some books fail during a basket import, the successful ones are imported and the failed ones stay in the basket so you can retry or remove them. The same book cannot be added twice **Bug fixes** - 🗓️ **Timezone-correct daily page statistics**: pages read between two progress updates are now attributed to calendar days in the user's timezone instead of fixed 24h slots, so the pages-per-day view matches your local days. Your heatmap may shift slightly after the upgrade