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
5 changes: 3 additions & 2 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ LibrisLog v1.8.0 brings camera selection and zoom control to the barcode scanner

<Badge type="warning" text="Unreleased" /> <Badge type="tip" text="Feature release" />

**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
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
100 changes: 96 additions & 4 deletions frontend/src/lib/components/AddBookModal.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts">
import type { AcquisitionStatus, Book, Medium, ReadingStatus } from '$lib/types';
import type { AcquisitionStatus, BasketItem, Book, Medium, ReadingStatus } from '$lib/types';
import { api } from '$lib/api';
import { _ } from '$lib/i18n';
import { toasts } from '$lib/toasts';
import ImportSearch from './ImportSearch.svelte';
import BasketPanel from './BasketPanel.svelte';
import BarcodeScanner from './BarcodeScanner.svelte';
import CoverPicker from './CoverPicker.svelte';
import TagInput from './TagInput.svelte';
import SuggestionInput from './SuggestionInput.svelte';
import { ScanBarcode, X } from '@lucide/svelte';
import { ScanBarcode, ShoppingBasket, X } from '@lucide/svelte';

let {
open = $bindable(false),
Expand All @@ -20,10 +21,12 @@
onAdded?: (book: Book) => void;
} = $props();

let activeTab = $state<'manual' | 'import'>('manual');
let activeTab = $state<'manual' | 'import' | 'basket'>('manual');
let submitting = $state(false);
let scannerOpen = $state(false);
let scannedIsbn = $state<string | null>(null);
let basket = $state<BasketItem[]>([]);
let basketImporting = $state(false);

// Manual form state
let title = $state('');
Expand Down Expand Up @@ -74,6 +77,72 @@
medium = '';
cover_url = null;
activeTab = 'manual';
basket = [];
}

function addToBasket(item: BasketItem) {
basket = [...basket, item];
}

function removeFromBasket(id: string) {
basket = basket.filter((item) => item.id !== id);
}

async function importBasket() {
if (basket.length === 0 || basketImporting) return;
basketImporting = true;
const items = basket;
const remaining: BasketItem[] = [];
const imported: Book[] = [];
let success = 0;
let failed = 0;

try {
for (const item of items) {
try {
const book = await api.import.importBook(
item.candidate,
item.readingStatus,
item.acquisitionStatus,
item.medium
);
imported.push(book);
success++;
} catch (e: unknown) {
remaining.push(item);
failed++;
const message =
e instanceof Error && e.message === 'error.isbnAlreadyExists'
? $_('error.isbnAlreadyExists')
: e instanceof Error
? e.message
: $_('import.importFailed');
toasts.add(message, 'error');
}
}

// Keep items added to the basket while the import was in flight.
basket = [...remaining, ...basket.filter((item) => !items.includes(item))];

// Notify the parent only after the whole run so a single-book
// import or a parent that closes the dialog on onAdded cannot
// interrupt the remaining items.
for (const book of imported) {
onAdded?.(book);
}

if (success > 0) {
toasts.add($_('import.basketImportSuccess', { values: { count: success } }), 'success');
}
if (failed === 0 && success > 0) {
open = false;
reset();
} else if (failed > 0) {
activeTab = 'basket';
}
} finally {
basketImporting = false;
}
}

async function submitManual() {
Expand Down Expand Up @@ -158,6 +227,19 @@
class="tab {activeTab === 'import' ? 'tab-active' : ''}"
onclick={() => (activeTab = 'import')}
>{$_('addModal.searchImport')}</button>
<button
role="tab"
class="tab {activeTab === 'basket' ? 'tab-active' : ''}"
onclick={() => (activeTab = 'basket')}
>
<span class="flex items-center gap-1">
<ShoppingBasket class="w-4 h-4" />
{$_('addModal.basket')}
{#if basket.length > 0}
<span class="badge badge-sm badge-primary ml-0.5">{basket.length}</span>
{/if}
</span>
</button>
</div>

{#if activeTab === 'manual'}
Expand Down Expand Up @@ -272,8 +354,11 @@
</button>
</div>
</form>
{:else}
{:else if activeTab === 'import'}
<ImportSearch
defaultStatus={defaultStatus}
basket={basket}
onAddToBasket={addToBasket}
onOpenScanner={() => {
scannerOpen = true;
}}
Expand All @@ -290,6 +375,13 @@
<div class="mt-3 text-center">
<a href="/data?tab=import" class="link link-primary text-sm">{$_('addModal.importFromFile')}</a>
</div>
{:else}
<BasketPanel
basket={basket}
importing={basketImporting}
onRemove={removeFromBasket}
onImport={importBasket}
/>
{/if}
</div>
<BarcodeScanner
Expand Down
Loading