From 6ce47af5d027ffc25dbd9dd2c60d5d057741bf4e Mon Sep 17 00:00:00 2001
From: codebude
Date: Wed, 26 Aug 2026 06:44:24 +0200
Subject: [PATCH 1/3] Renamed availability to possession
---
backend/app/routers/books.py | 2 +-
backend/app/services/search.py | 14 ++++++------
backend/tests/test_books.py | 12 +++++-----
backend/tests/test_search_query.py | 22 +++++++++----------
docs/guide/using-librislog/library.md | 4 ++--
docs/guide/using-librislog/search.md | 10 ++++-----
.../e2e/specs/03-library-browsing.spec.ts | 6 ++---
.../src/lib/components/AddBookModal.test.ts | 2 +-
frontend/src/lib/components/SearchHelp.svelte | 4 ++--
.../src/lib/components/SearchHelp.test.ts | 2 +-
frontend/src/lib/i18n/locales/de.json | 2 +-
frontend/src/lib/i18n/locales/en.json | 6 ++---
frontend/src/lib/i18n/locales/es.json | 2 +-
frontend/src/lib/i18n/locales/fr.json | 2 +-
frontend/src/lib/i18n/locales/zh.json | 2 +-
15 files changed, 46 insertions(+), 46 deletions(-)
diff --git a/backend/app/routers/books.py b/backend/app/routers/books.py
index c6c5e8de..aceadb64 100644
--- a/backend/app/routers/books.py
+++ b/backend/app/routers/books.py
@@ -156,7 +156,7 @@ def list_books(
default=None,
description=(
"Search phrase. Use : to restrict a term to a single field "
- "(author, publisher, title, tag, language, availability, notes, description). "
+ "(author, publisher, title, tag, language, possession, notes, description). "
"Wrap multi-word values in double quotes (e.g. author:\"Marlen Haushofer\") and "
"prefix any term with - to negate it (e.g. tag:cars -tag:audi)."
),
diff --git a/backend/app/services/search.py b/backend/app/services/search.py
index 65117959..cc1a757a 100644
--- a/backend/app/services/search.py
+++ b/backend/app/services/search.py
@@ -28,13 +28,13 @@
"description": Book.blurb,
}
-# Availability is a special case: it maps to an exact enum comparison.
-AVAILABILITY_PREFIX = "availability"
+# Possession is a special case: it maps to an exact enum comparison.
+POSSESSION_PREFIX = "possession"
TAG_PREFIX = "tag"
AUTHOR_PREFIX = "author"
SUPPORTED_PREFIXES: frozenset[str] = frozenset(
- [*FIELD_COLUMNS.keys(), AVAILABILITY_PREFIX, TAG_PREFIX, AUTHOR_PREFIX]
+ [*FIELD_COLUMNS.keys(), POSSESSION_PREFIX, TAG_PREFIX, AUTHOR_PREFIX]
)
# Default fields searched by an unprefixed term (unchanged from the previous
@@ -168,7 +168,7 @@ def _unprefixed_condition(value: str, user_id: int) -> Any:
)
-def _availability_condition(value: str) -> Any | None:
+def _possession_condition(value: str) -> Any | None:
"""Build the exact acquisition-status condition, or ``None`` if invalid."""
normalized = value.strip().lower().replace(" ", "_")
try:
@@ -180,8 +180,8 @@ def _availability_condition(value: str) -> Any | None:
def _field_condition(field: str, value: str, user_id: int) -> Any | None:
"""Build the condition for a single field-specific term."""
- if field == AVAILABILITY_PREFIX:
- return _availability_condition(value)
+ if field == POSSESSION_PREFIX:
+ return _possession_condition(value)
if field == TAG_PREFIX:
return _tag_condition(value, user_id)
if field == AUTHOR_PREFIX:
@@ -212,7 +212,7 @@ def apply_search_filter(statement: Any, query: str, user_id: int) -> Any:
condition = _field_condition(term.field, term.value, user_id)
if condition is None:
- # Invalid availability value: positive yields no rows, negated is a no-op.
+ # Invalid possession value: positive yields no rows, negated is a no-op.
conditions.append(sa.false() if not term.negated else sa.true())
elif term.negated:
conditions.append(sa.not_(condition))
diff --git a/backend/tests/test_books.py b/backend/tests/test_books.py
index 32567309..c2eb0c65 100644
--- a/backend/tests/test_books.py
+++ b/backend/tests/test_books.py
@@ -281,19 +281,19 @@ def test_list_books_search_by_language(client: TestClient) -> None:
assert body["books"][0]["title"] == "Dune"
-def test_list_books_search_by_availability(client: TestClient) -> None:
+def test_list_books_search_by_possession(client: TestClient) -> None:
_create_book(client, title="Borrowed", acquisition_status="borrowed")
_create_book(client, title="Owned", acquisition_status="owned")
- resp = client.get("/api/books?q=availability:borrowed")
+ resp = client.get("/api/books?q=possession:borrowed")
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 1
assert body["books"][0]["title"] == "Borrowed"
-def test_list_books_search_by_availability_invalid_value(client: TestClient) -> None:
+def test_list_books_search_by_possession_invalid_value(client: TestClient) -> None:
_create_book(client, title="Borrowed", acquisition_status="borrowed")
- resp = client.get("/api/books?q=availability:not-a-status")
+ resp = client.get("/api/books?q=possession:not-a-status")
assert resp.status_code == 200
assert resp.json()["total"] == 0
@@ -393,10 +393,10 @@ def test_list_books_search_negation_includes_nullable_field_rows(client: TestCli
assert [b["title"] for b in body["books"]] == ["Plain"]
-def test_list_books_search_availability_quoted_multiword(client: TestClient) -> None:
+def test_list_books_search_possession_quoted_multiword(client: TestClient) -> None:
_create_book(client, title="Wanted", acquisition_status="to_acquire")
_create_book(client, title="Owned", acquisition_status="owned")
- resp = client.get('/api/books?q=availability:"to acquire"')
+ resp = client.get('/api/books?q=possession:"to acquire"')
assert resp.status_code == 200
body = resp.json()
assert [b["title"] for b in body["books"]] == ["Wanted"]
diff --git a/backend/tests/test_search_query.py b/backend/tests/test_search_query.py
index ad4af3c9..a288f4c3 100644
--- a/backend/tests/test_search_query.py
+++ b/backend/tests/test_search_query.py
@@ -86,7 +86,7 @@ def test_parse_bare_prefix() -> None:
def test_parse_all_supported_prefixes() -> None:
- query = "author:a title:t publisher:p tag:g language:en availability:owned notes:n description:d"
+ query = "author:a title:t publisher:p tag:g language:en possession:owned notes:n description:d"
fields = [t.field for t in parse_search_query(query)]
assert fields == [
"author",
@@ -94,22 +94,22 @@ def test_parse_all_supported_prefixes() -> None:
"publisher",
"tag",
"language",
- "availability",
+ "possession",
"notes",
"description",
]
-def test_availability_condition_accepts_enum_values() -> None:
- from app.services.search import _availability_condition
+def test_possession_condition_accepts_enum_values() -> None:
+ from app.services.search import _possession_condition
- assert _availability_condition("owned") is not None
- assert _availability_condition("digital_access") is not None
- assert _availability_condition("to acquire") is not None
- assert _availability_condition("owned") is not None
+ assert _possession_condition("owned") is not None
+ assert _possession_condition("digital_access") is not None
+ assert _possession_condition("to acquire") is not None
+ assert _possession_condition("owned") is not None
-def test_availability_condition_rejects_unknown_value() -> None:
- from app.services.search import _availability_condition
+def test_possession_condition_rejects_unknown_value() -> None:
+ from app.services.search import _possession_condition
- assert _availability_condition("not-a-status") is None
\ No newline at end of file
+ assert _possession_condition("not-a-status") is None
\ No newline at end of file
diff --git a/docs/guide/using-librislog/library.md b/docs/guide/using-librislog/library.md
index d8bdf321..df4962cd 100644
--- a/docs/guide/using-librislog/library.md
+++ b/docs/guide/using-librislog/library.md
@@ -15,9 +15,9 @@ Books are categorized into four statuses:
Each status has its own tab in the library view, making it easy to browse your collection by reading state.
-## Availability
+## Possession
-Availability is separate from reading status. Choose whether a book is owned, borrowed, available digitally, or still needs to be acquired. In the Want to Read view, books that still need to be acquired show a shopping-cart indicator. Use the availability filter to narrow the list without changing its newest-first order.
+Possession is separate from reading status. Choose whether a book is owned, borrowed, available digitally, or still needs to be acquired. In the Want to Read view, books that still need to be acquired show a shopping-cart indicator. Use the possession filter to narrow the list without changing its newest-first order.

diff --git a/docs/guide/using-librislog/search.md b/docs/guide/using-librislog/search.md
index 8e08d2e1..abdfefac 100644
--- a/docs/guide/using-librislog/search.md
+++ b/docs/guide/using-librislog/search.md
@@ -13,7 +13,7 @@ Use `:` to search in a single field. The field prefixes are always
| `publisher` | Publisher | `publisher:Penguin` |
| `language` | Language | `language:Japanese` |
| `tag` | Tag name | `tag:fantasy` |
-| `availability` | Acquisition status | `availability:owned` |
+| `possession` | Possession status | `possession:owned` |
| `notes` | Private notes | `notes:"to reread"` |
| `description` | Blurb / description | `description:"middle earth"` |
@@ -21,16 +21,16 @@ Use quotes for values that contain spaces: `title:"The Silmarillion"`.
The `author:` prefix matches **any** author assigned to a book — a book with multiple authors matches if any of them contains the search value.
-### Availability values
+### Possession values
-The `availability` prefix matches the exact acquisition status. Accepted values include:
+The `possession` prefix matches the exact possession status. Accepted values include:
- `to_acquire` (or `to acquire`)
- `owned`
- `borrowed`
- `digital`
-Example: `availability:"to acquire"` shows books you want to buy.
+Example: `possession:"to acquire"` shows books you want to buy.
## Negation
@@ -45,7 +45,7 @@ Prefix a term with `-` to exclude matches.
Separate terms with spaces. All terms are combined with **AND**.
- `author:Murakami -title:Norwegian` — Murakami books except those whose title contains "Norwegian"
-- `tag:fantasy availability:owned` — owned fantasy books
+- `tag:fantasy possession:owned` — owned fantasy books
## Plain text
diff --git a/frontend/e2e/specs/03-library-browsing.spec.ts b/frontend/e2e/specs/03-library-browsing.spec.ts
index 0971e9c6..9b56f43b 100644
--- a/frontend/e2e/specs/03-library-browsing.spec.ts
+++ b/frontend/e2e/specs/03-library-browsing.spec.ts
@@ -67,14 +67,14 @@ test.describe('Library Browsing', () => {
await expect(body).toContainText(/no books|empty/i);
});
- test('3.5 manual creation requires availability and persists the selected value', async ({ page }) => {
+ test('3.5 manual creation requires possession and persists the selected value', async ({ page }) => {
await deleteAllBooks(page);
const library = new LibraryPage(page);
await library.goto();
await page.getByRole('button', { name: '+ Add Book' }).click();
const modal = page.locator('.modal-box');
- const availability = modal.getByRole('combobox', { name: /Availability/ });
+ const availability = modal.getByRole('combobox', { name: /Possession/ });
await expect(availability).toHaveValue('');
await modal.getByLabel('Title *').fill('Digital E2E Book');
@@ -91,7 +91,7 @@ test.describe('Library Browsing', () => {
expect((await response.json()).books[0].acquisition_status).toBe('digital_access');
});
- test('3.6 filters Want to Read books by availability and marks books to acquire', async ({ page }) => {
+ test('3.6 filters Want to Read books by possession and marks books to acquire', async ({ page }) => {
await deleteAllBooks(page);
await createWantToReadBook(page, 'Owned E2E Book', 'owned');
await createWantToReadBook(page, 'Acquire E2E Book', 'to_acquire');
diff --git a/frontend/src/lib/components/AddBookModal.test.ts b/frontend/src/lib/components/AddBookModal.test.ts
index bdac0797..698eff31 100644
--- a/frontend/src/lib/components/AddBookModal.test.ts
+++ b/frontend/src/lib/components/AddBookModal.test.ts
@@ -128,7 +128,7 @@ describe('AddBookModal', () => {
fireEvent.keyDown(authorInput, { key: 'Enter' });
const pagesInput = screen.getByLabelText(/Pages/);
fireEvent.input(pagesInput, { target: { value: '412' } });
- fireEvent.change(screen.getByRole('combobox', { name: /Availability/ }), { target: { value: 'owned' } });
+ fireEvent.change(screen.getByRole('combobox', { name: /Possession/ }), { target: { value: 'owned' } });
}
it('submits form and calls api.books.create', async () => {
diff --git a/frontend/src/lib/components/SearchHelp.svelte b/frontend/src/lib/components/SearchHelp.svelte
index fc56fce3..d058c455 100644
--- a/frontend/src/lib/components/SearchHelp.svelte
+++ b/frontend/src/lib/components/SearchHelp.svelte
@@ -20,7 +20,7 @@
{ name: 'title', example: 'title:fragezeichen' },
{ name: 'tag', example: 'tag:cars' },
{ name: 'language', example: 'language:en' },
- { name: 'availability', example: 'availability:owned' },
+ { name: 'possession', example: 'possession:owned' },
{ name: 'notes', example: 'notes:reading' },
{ name: 'description', example: 'description:desert' }
];
@@ -74,7 +74,7 @@
{$_('search.help.negate')}
- {$_('search.help.availabilityValues')}
+ {$_('search.help.possessionValues')}
{/if}
diff --git a/frontend/src/lib/components/SearchHelp.test.ts b/frontend/src/lib/components/SearchHelp.test.ts
index 1c319479..9bac602a 100644
--- a/frontend/src/lib/components/SearchHelp.test.ts
+++ b/frontend/src/lib/components/SearchHelp.test.ts
@@ -33,7 +33,7 @@ describe('SearchHelp', () => {
expect(dialog).toHaveTextContent('title');
expect(dialog).toHaveTextContent('tag');
expect(dialog).toHaveTextContent('language');
- expect(dialog).toHaveTextContent('availability');
+ expect(dialog).toHaveTextContent('possession');
expect(dialog).toHaveTextContent('notes');
expect(dialog).toHaveTextContent('description');
});
diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json
index 480bf7b7..e94997aa 100644
--- a/frontend/src/lib/i18n/locales/de.json
+++ b/frontend/src/lib/i18n/locales/de.json
@@ -303,7 +303,7 @@
"multiWord": "Setze mehrteilige Werte in doppelte Anführungszeichen, z. B. author:\"Marlen Haushofer\".",
"combine": "Du kannst mehrere Präfixe kombinieren; die Ergebnisse müssen allen entsprechen.",
"negate": "Stelle einem Begriff ein - voran, um ihn auszuschließen, z. B. tag:cars -tag:audi.",
- "availabilityValues": "Verfügbarkeitswerte: owned, borrowed, digital_access, to_acquire"
+ "possessionValues": "Possession-Werte: owned, borrowed, digital_access, to_acquire"
}
},
"languages": {
diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json
index c75b2a74..12f73855 100644
--- a/frontend/src/lib/i18n/locales/en.json
+++ b/frontend/src/lib/i18n/locales/en.json
@@ -147,7 +147,7 @@
"blurb": "Description",
"about": "About",
"dateStarted": "Date started",
- "acquisitionStatus": "Availability",
+ "acquisitionStatus": "Possession",
"selectAcquisitionStatus": "Select availability...",
"startDatePromptTitle": "Set a start date?",
"startDatePromptMessage": "This book has no start date. Set it to today or choose another date before marking it as read.",
@@ -303,7 +303,7 @@
"multiWord": "Wrap multi-word values in double quotes, e.g. author:\"Marlen Haushofer\".",
"combine": "You can combine multiple prefixes; results must match all of them.",
"negate": "Prefix any term with - to exclude it, e.g. tag:cars -tag:audi.",
- "availabilityValues": "availability values: owned, borrowed, digital_access, to_acquire"
+ "possessionValues": "possession values: owned, borrowed, digital_access, to_acquire"
}
},
"languages": {
@@ -722,4 +722,4 @@
"candidatesError": "Cover search failed. You can still use manual import.",
"retry": "Retry"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json
index 3a1f4e9d..6a8d84dc 100644
--- a/frontend/src/lib/i18n/locales/es.json
+++ b/frontend/src/lib/i18n/locales/es.json
@@ -303,7 +303,7 @@
"multiWord": "Envuelve los valores de varias palabras entre comillas dobles, p. ej. author:\"Marlen Haushofer\".",
"combine": "Puedes combinar varios prefijos; los resultados deben coincidir con todos.",
"negate": "Antepón - a cualquier término para excluirlo, p. ej. tag:cars -tag:audi.",
- "availabilityValues": "valores de disponibilidad: owned, borrowed, digital_access, to_acquire"
+ "possessionValues": "valores de posesión: owned, borrowed, digital_access, to_acquire"
}
},
"languages": {
diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json
index ecbf445f..1cc1c20c 100644
--- a/frontend/src/lib/i18n/locales/fr.json
+++ b/frontend/src/lib/i18n/locales/fr.json
@@ -303,7 +303,7 @@
"multiWord": "Place les valeurs multi-mots entre guillemets doubles, p. ex. author:\"Marlen Haushofer\".",
"combine": "Tu peux combiner plusieurs préfixes ; les résultats doivent correspondre à tous.",
"negate": "Préfixe tout terme par - pour l'exclure, p. ex. tag:cars -tag:audi.",
- "availabilityValues": "valeurs de disponibilité : owned, borrowed, digital_access, to_acquire"
+ "possessionValues": "valeurs de possession : owned, borrowed, digital_access, to_acquire"
}
},
"languages": {
diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json
index 8fc59cf4..d2e90505 100644
--- a/frontend/src/lib/i18n/locales/zh.json
+++ b/frontend/src/lib/i18n/locales/zh.json
@@ -303,7 +303,7 @@
"multiWord": "多词值请用双引号括起来,例如 author:\"Marlen Haushofer\"。",
"combine": "可以组合多个前缀;结果必须满足所有条件。",
"negate": "在任何词条前加 - 以将其排除,例如 tag:cars -tag:audi。",
- "availabilityValues": "可用性值:owned、borrowed、digital_access、to_acquire"
+ "possessionValues": "持有值:owned、borrowed、digital_access、to_acquire"
}
},
"languages": {
From 16834edc35d8c171c6d7e064202b5f6aff9762db Mon Sep 17 00:00:00 2001
From: codebude
Date: Wed, 26 Aug 2026 10:12:21 +0200
Subject: [PATCH 2/3] New all books tab on library page
---
docs/guide/using-librislog/library.md | 2 +-
.../e2e/specs/03-library-browsing.spec.ts | 32 +++
frontend/src/lib/i18n/locales/de.json | 3 +-
frontend/src/lib/i18n/locales/en.json | 3 +-
frontend/src/lib/i18n/locales/es.json | 3 +-
frontend/src/lib/i18n/locales/fr.json | 3 +-
frontend/src/lib/i18n/locales/zh.json | 3 +-
frontend/src/routes/library/+page.svelte | 44 ++--
frontend/src/routes/library/page.test.ts | 188 ++++++++++++++++++
9 files changed, 256 insertions(+), 25 deletions(-)
create mode 100644 frontend/src/routes/library/page.test.ts
diff --git a/docs/guide/using-librislog/library.md b/docs/guide/using-librislog/library.md
index df4962cd..a02c2c0a 100644
--- a/docs/guide/using-librislog/library.md
+++ b/docs/guide/using-librislog/library.md
@@ -13,7 +13,7 @@ Books are categorized into four statuses:
| **Read** | Books you've finished |
| **Did Not Finish** | Books you started but abandoned |
-Each status has its own tab in the library view, making it easy to browse your collection by reading state.
+Each status has its own tab in the library view, making it easy to browse your collection by reading state. A fifth **All Books** tab shows every book regardless of status; like the other tabs it supports search and sorting (smart sort is disabled there, since it's based on per-status defaults).
## Possession
diff --git a/frontend/e2e/specs/03-library-browsing.spec.ts b/frontend/e2e/specs/03-library-browsing.spec.ts
index 9b56f43b..b1735d7d 100644
--- a/frontend/e2e/specs/03-library-browsing.spec.ts
+++ b/frontend/e2e/specs/03-library-browsing.spec.ts
@@ -106,4 +106,36 @@ test.describe('Library Browsing', () => {
await expect(page.getByText('Acquire E2E Book')).toBeVisible();
await expect(page.getByText('Owned E2E Book')).not.toBeVisible();
});
+
+ test('3.7 All Books tab shows every book regardless of status with search and sort', async ({ page }) => {
+ const library = new LibraryPage(page);
+ await library.goto();
+
+ await page.getByRole('tab', { name: /All Books/ }).click();
+ await expect(page).toHaveURL(/\/library\?status=all/);
+
+ // All 12 seeded books are shown, spanning every reading status.
+ await expect(library.getBookCards()).toHaveCount(12);
+ await expect(page.getByText('The Great Gatsby')).toBeVisible();
+ await expect(page.getByText('The Three-Body Problem')).toBeVisible();
+ await expect(page.getByText('1984')).toBeVisible();
+ await expect(page.getByText('Atlas Shrugged')).toBeVisible();
+
+ // Smart sort is hidden on the All tab; the sort selects are enabled.
+ await expect(page.locator('input[name="smart-sort"]')).toHaveCount(0);
+ await expect(page.locator('select[name="sort-field"]')).toBeEnabled();
+
+ // Sort by title ascending: "1984" is alphabetically first among the seeds.
+ await page.locator('select[name="sort-field"]').selectOption('title');
+ await page.locator('select[name="sort-order"]').selectOption('asc');
+ await expect(page.locator('button.card h2').first()).toHaveText('1984');
+
+ // Search narrows the All tab results.
+ const searchInput = page.getByPlaceholder(/Search books/);
+ await searchInput.fill('Dune');
+ await searchInput.press('Enter');
+ await expect(page.locator('button.card')).toHaveCount(1);
+ await expect(page.getByText('Dune')).toBeVisible();
+ await expect(page.getByText('1984')).not.toBeVisible();
+ });
});
diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json
index e94997aa..5d4ee6e9 100644
--- a/frontend/src/lib/i18n/locales/de.json
+++ b/frontend/src/lib/i18n/locales/de.json
@@ -84,7 +84,8 @@
"want_to_read": "Möchte ich lesen",
"currently_reading": "Lese ich gerade",
"read": "Gelesen",
- "did_not_finish": "Abgebrochen"
+ "did_not_finish": "Abgebrochen",
+ "all": "Alle Bücher"
},
"common": {
"all": "Alle",
diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json
index 12f73855..1c9d0376 100644
--- a/frontend/src/lib/i18n/locales/en.json
+++ b/frontend/src/lib/i18n/locales/en.json
@@ -84,7 +84,8 @@
"want_to_read": "Want to Read",
"currently_reading": "Currently Reading",
"read": "Read",
- "did_not_finish": "Did Not Finish"
+ "did_not_finish": "Did Not Finish",
+ "all": "All Books"
},
"common": {
"all": "All",
diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json
index 6a8d84dc..d3bc2a6e 100644
--- a/frontend/src/lib/i18n/locales/es.json
+++ b/frontend/src/lib/i18n/locales/es.json
@@ -84,7 +84,8 @@
"want_to_read": "Quiero leer",
"currently_reading": "Leyendo",
"read": "Leído",
- "did_not_finish": "Abandonado"
+ "did_not_finish": "Abandonado",
+ "all": "Todos los libros"
},
"common": {
"all": "Todos",
diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json
index 1cc1c20c..daea5c23 100644
--- a/frontend/src/lib/i18n/locales/fr.json
+++ b/frontend/src/lib/i18n/locales/fr.json
@@ -84,7 +84,8 @@
"want_to_read": "À lire",
"currently_reading": "En cours",
"read": "Lu",
- "did_not_finish": "Abandonné"
+ "did_not_finish": "Abandonné",
+ "all": "Tous les livres"
},
"common": {
"all": "Tous",
diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json
index d2e90505..15b1f227 100644
--- a/frontend/src/lib/i18n/locales/zh.json
+++ b/frontend/src/lib/i18n/locales/zh.json
@@ -84,7 +84,8 @@
"want_to_read": "想读",
"currently_reading": "在读",
"read": "已读",
- "did_not_finish": "弃读"
+ "did_not_finish": "弃读",
+ "all": "所有书籍"
},
"common": {
"all": "全部",
diff --git a/frontend/src/routes/library/+page.svelte b/frontend/src/routes/library/+page.svelte
index 9a2c26e9..a3e3fd68 100644
--- a/frontend/src/routes/library/+page.svelte
+++ b/frontend/src/routes/library/+page.svelte
@@ -14,10 +14,10 @@
import AddBookModal from '$lib/components/AddBookModal.svelte';
import SearchBar from '$lib/components/SearchBar.svelte';
import SearchHelp from '$lib/components/SearchHelp.svelte';
- import { BookOpen as BookOpenIcon, Book as BookIcon, Check, X } from '@lucide/svelte';
+ import { BookOpen as BookOpenIcon, Book as BookIcon, Check, Library, X } from '@lucide/svelte';
type Tab = {
- status: ReadingStatus;
+ status: ReadingStatus | 'all';
labelKey: string;
Icon: typeof BookOpenIcon;
};
@@ -26,20 +26,22 @@
{ status: 'want_to_read', labelKey: 'status.want_to_read', Icon: BookOpenIcon },
{ status: 'currently_reading', labelKey: 'status.currently_reading', Icon: BookIcon },
{ status: 'read', labelKey: 'status.read', Icon: Check },
- { status: 'did_not_finish', labelKey: 'status.did_not_finish', Icon: X }
+ { status: 'did_not_finish', labelKey: 'status.did_not_finish', Icon: X },
+ { status: 'all', labelKey: 'status.all', Icon: Library }
];
const STATUS_LABEL_KEYS: Record = {
want_to_read: 'status.want_to_read',
currently_reading: 'status.currently_reading',
read: 'status.read',
- did_not_finish: 'status.did_not_finish'
+ did_not_finish: 'status.did_not_finish',
+ all: 'status.all'
};
const PAGE_SIZE = 40;
- let activeStatus = $derived(
- ($page.url.searchParams.get('status') as ReadingStatus) ?? 'want_to_read'
+ let activeStatus = $derived(
+ ($page.url.searchParams.get('status') as ReadingStatus | 'all') ?? 'want_to_read'
);
let requestedBookId = $derived.by(() => {
const raw = $page.url.searchParams.get('bookId');
@@ -85,7 +87,7 @@
return numberFormatter.format(value);
}
- function getStatusCount(status: ReadingStatus): number | null {
+ function getStatusCount(status: ReadingStatus | 'all'): number | null {
if (!statusCounts) return null;
switch (status) {
case 'want_to_read':
@@ -96,6 +98,8 @@
return statusCounts.books_read;
case 'did_not_finish':
return statusCounts.books_did_not_finish;
+ case 'all':
+ return statusCounts.total_books;
}
}
@@ -112,7 +116,7 @@
let drawerOpen = $state(false);
let addBookOpen = $state(false);
- function changeTab(status: ReadingStatus) {
+ function changeTab(status: ReadingStatus | 'all') {
if (status === activeStatus) return;
void goto(`/library?status=${status}`);
}
@@ -156,7 +160,7 @@
loading = true;
try {
const response = await api.books.list({
- status: activeStatus,
+ status: activeStatus === 'all' ? undefined : activeStatus,
acquisition_status: activeStatus === 'want_to_read' && acquisitionFilter ? acquisitionFilter : undefined,
q: searchQuery || undefined,
smart_sort: smartSort,
@@ -190,7 +194,7 @@
loadingMore = true;
try {
const response = await api.books.list({
- status: activeStatus,
+ status: activeStatus === 'all' ? undefined : activeStatus,
acquisition_status: activeStatus === 'want_to_read' && acquisitionFilter ? acquisitionFilter : undefined,
q: searchQuery || undefined,
smart_sort: smartSort,
@@ -252,7 +256,7 @@
function handleSave(updated: Book) {
selectedBook = updated;
- if (updated.reading_status !== activeStatus) {
+ if (activeStatus !== 'all' && updated.reading_status !== activeStatus) {
detailOpen = false;
drawerOpen = false;
books = books.filter((b) => b.id !== updated.id);
@@ -274,7 +278,7 @@
}
function handleAdded(book: Book) {
- if (book.reading_status === activeStatus) {
+ if (activeStatus === 'all' || book.reading_status === activeStatus) {
books = [book, ...books];
}
addBookOpen = false;
@@ -401,18 +405,20 @@
-
-