diff --git a/backend/alembic/versions/d7e8f9a0b1c2_add_reading_date_automation_to_usersettings.py b/backend/alembic/versions/d7e8f9a0b1c2_add_reading_date_automation_to_usersettings.py new file mode 100644 index 00000000..1ad11b3d --- /dev/null +++ b/backend/alembic/versions/d7e8f9a0b1c2_add_reading_date_automation_to_usersettings.py @@ -0,0 +1,33 @@ +"""add reading date automation settings + +Revision ID: d7e8f9a0b1c2 +Revises: 0a1b2c3d4e5f +Create Date: 2026-09-14 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "d7e8f9a0b1c2" +down_revision: Union[str, Sequence[str], None] = "0a1b2c3d4e5f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "usersettings", + sa.Column("auto_set_date_started", sa.Boolean(), nullable=False, server_default=sa.true()), + ) + op.add_column( + "usersettings", + sa.Column("auto_set_date_finished", sa.Boolean(), nullable=False, server_default=sa.true()), + ) + + +def downgrade() -> None: + op.drop_column("usersettings", "auto_set_date_finished") + op.drop_column("usersettings", "auto_set_date_started") diff --git a/backend/app/models.py b/backend/app/models.py index 81a7affc..88e5db2d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -221,6 +221,8 @@ class UserSettings(SQLModel, table=True): goal_books_per_year_enabled: bool = Field(default=False) goal_books_per_year: int = Field(default=25, ge=1) gamification_enabled: bool = Field(default=True) + auto_set_date_started: bool = Field(default=True) + auto_set_date_finished: bool = Field(default=True) statistics_range: str = Field(default="alltime", max_length=20) statistics_custom_from: Optional[date] = Field(default=None) statistics_custom_to: Optional[date] = Field(default=None) diff --git a/backend/app/routers/books.py b/backend/app/routers/books.py index a1ca29f0..f311813c 100644 --- a/backend/app/routers/books.py +++ b/backend/app/routers/books.py @@ -12,7 +12,7 @@ from app.auth import require_user from app.config import settings from app.database import get_session -from app.models import AcquisitionStatus, Author, Book, BookAuthor, BookTag, Medium, ReadingProgress, ReadingStatus, Tag, User +from app.models import AcquisitionStatus, Author, Book, BookAuthor, BookTag, Medium, ReadingProgress, ReadingStatus, Tag, User, UserSettings from app.schemas import ( BookCreate, BookListResponse, @@ -60,23 +60,36 @@ def _utcnow() -> datetime: return utcnow() +def _reading_date_automation_settings(session: Session, user_id: int) -> tuple[bool, bool]: + """Return start/finish date automation preferences, defaulting to enabled.""" + settings = session.exec( + select(UserSettings).where(UserSettings.user_id == user_id) + ).first() + if settings is None: + return True, True + return settings.auto_set_date_started, settings.auto_set_date_finished + + def _apply_status_transition_dates( book: Book, target_status: ReadingStatus, update_data: dict, skip_auto_date_started: bool = False, + *, + auto_set_date_started: bool = True, + auto_set_date_finished: bool = True, ) -> None: """Auto-fill date_started / date_finished when transitioning to a new status.""" if target_status == book.reading_status: return - if target_status == ReadingStatus.currently_reading: + if target_status == ReadingStatus.currently_reading and auto_set_date_started: if skip_auto_date_started: update_data.setdefault("date_started", None) elif book.date_started is None and update_data.get("date_started") is None: update_data["date_started"] = _utcnow() - if target_status in (ReadingStatus.read, ReadingStatus.did_not_finish): + if target_status in (ReadingStatus.read, ReadingStatus.did_not_finish) and auto_set_date_finished: if update_data.get("date_finished") is None: update_data["date_finished"] = _utcnow() @@ -105,6 +118,7 @@ def _validate_date_finished_for_read( book: Book, update_data: dict, target_status: ReadingStatus, + auto_set_date_finished: bool = True, ) -> None: """Ensure date_finished is not explicitly cleared while the book is read.""" if "date_finished" not in update_data: @@ -113,7 +127,11 @@ def _validate_date_finished_for_read( return if book.date_finished is None: return - if book.reading_status == ReadingStatus.read and target_status == ReadingStatus.read: + if ( + auto_set_date_finished + and book.reading_status == ReadingStatus.read + and target_status == ReadingStatus.read + ): raise HTTPException(status_code=422, detail="A finished book must have an end date. Change the status if you want to remove the finish date.") @@ -242,11 +260,14 @@ def list_books( book_ids = [b.id for b in books if b.id is not None] book_tags_map = load_tags_batch(session, book_ids) if book_ids else {} book_authors_map = load_authors_batch(session, book_ids) if book_ids else {} - return BookListResponse( - books=[ + book_reads: list[BookRead] = [] + for book in books: + assert book.id is not None + book_reads.append( _build_book_read_with_tags(book, book_tags_map.get(book.id), book_authors_map.get(book.id)) - for book in books - ], + ) + return BookListResponse( + books=book_reads, total=total, ) @@ -511,6 +532,9 @@ async def update_book( ) authors_provided = authors_payload is not None target_status = update_data.get("reading_status", book.reading_status) + auto_set_date_started, auto_set_date_finished = _reading_date_automation_settings( + session, current_user.id + ) # Download external cover URL -> local file. if "cover_url" in update_data and is_external_cover_url(update_data["cover_url"]): @@ -540,9 +564,20 @@ async def update_book( if not shared: delete_cover_file(old_filename, settings.covers_dir) - _apply_status_transition_dates(book, target_status, update_data) + _apply_status_transition_dates( + book, + target_status, + update_data, + auto_set_date_started=auto_set_date_started, + auto_set_date_finished=auto_set_date_finished, + ) _validate_dates(update_data) - _validate_date_finished_for_read(book, update_data, target_status) + _validate_date_finished_for_read( + book, + update_data, + target_status, + auto_set_date_finished=auto_set_date_finished, + ) book.sqlmodel_update(update_data) session.add(book) @@ -577,6 +612,7 @@ def transition_status( session: Session = Depends(get_session), ) -> StatusTransitionResponse: """Change a book's reading status with date-conflict detection and resolution.""" + assert current_user.id is not None logger.debug( "transition_status — id=%s new_status=%s force_date_started=%r force_date_finished=%r", book_id, transition.new_status, transition.force_date_started, transition.force_date_finished, @@ -588,6 +624,9 @@ def transition_status( conflict: DateConflict | None = None update_data: dict = {"reading_status": transition.new_status} now = _utcnow() + auto_set_date_started, auto_set_date_finished = _reading_date_automation_settings( + session, current_user.id + ) # date_finished handling is split into two passes: # 1. Inline below — conflict detection when moving TO read/did_not_finish @@ -602,17 +641,17 @@ def transition_status( and book.date_started is not None and not transition.skip_auto_date_started ): - if transition.force_date_started is None: + if transition.force_date_started is None and auto_set_date_started: conflict = DateConflict( field="date_started", existing_date=book.date_started, suggested_date=now, ) return StatusTransitionResponse(book=build_book_read(session, book), date_conflict=conflict) - update_data["date_started"] = transition.force_date_started if ( - book.date_finished is not None + transition.force_date_started is not None + and book.date_finished is not None and transition.force_date_started > book.date_finished ): conflict = DateConflict( @@ -642,6 +681,7 @@ def transition_status( and book.date_started is None and book.date_finished is not None and transition.force_date_started is None + and auto_set_date_started and not transition.skip_auto_date_started ): conflict = DateConflict( @@ -681,9 +721,21 @@ def transition_status( if transition.force_date_finished is not None: update_data["date_finished"] = transition.force_date_finished - _apply_status_transition_dates(book, transition.new_status, update_data, transition.skip_auto_date_started) + _apply_status_transition_dates( + book, + transition.new_status, + update_data, + transition.skip_auto_date_started, + auto_set_date_started=auto_set_date_started, + auto_set_date_finished=auto_set_date_finished, + ) _validate_dates(update_data) - _validate_date_finished_for_read(book, update_data, transition.new_status) + _validate_date_finished_for_read( + book, + update_data, + transition.new_status, + auto_set_date_finished=auto_set_date_finished, + ) book.sqlmodel_update(update_data) session.add(book) session.commit() diff --git a/backend/app/routers/profile.py b/backend/app/routers/profile.py index 5f4ef840..4358f08d 100644 --- a/backend/app/routers/profile.py +++ b/backend/app/routers/profile.py @@ -32,6 +32,7 @@ EmbedTokenRead, EmbedTokenUpdate, ProfileUpdate, + StatisticsRange, UserRead, UserSettingsRead, UserSettingsUpdate, @@ -99,6 +100,7 @@ def get_settings( session.add(settings) session.commit() session.refresh(settings) + assert settings.user_id is not None return UserSettingsRead( user_id=settings.user_id, language=settings.language, @@ -114,7 +116,9 @@ def get_settings( goal_books_per_year_enabled=settings.goal_books_per_year_enabled, goal_books_per_year=settings.goal_books_per_year, gamification_enabled=settings.gamification_enabled, - statistics_range=settings.statistics_range, + auto_set_date_started=settings.auto_set_date_started, + auto_set_date_finished=settings.auto_set_date_finished, + statistics_range=StatisticsRange(settings.statistics_range), statistics_custom_from=settings.statistics_custom_from, statistics_custom_to=settings.statistics_custom_to, ) @@ -151,6 +155,7 @@ def update_settings( session.add(settings) session.commit() session.refresh(settings) + assert settings.user_id is not None return UserSettingsRead( user_id=settings.user_id, language=settings.language, @@ -166,7 +171,9 @@ def update_settings( goal_books_per_year_enabled=settings.goal_books_per_year_enabled, goal_books_per_year=settings.goal_books_per_year, gamification_enabled=settings.gamification_enabled, - statistics_range=settings.statistics_range, + auto_set_date_started=settings.auto_set_date_started, + auto_set_date_finished=settings.auto_set_date_finished, + statistics_range=StatisticsRange(settings.statistics_range), statistics_custom_from=settings.statistics_custom_from, statistics_custom_to=settings.statistics_custom_to, ) @@ -369,7 +376,7 @@ def rotate_embed_token( token.revoked_at = now session.add(token) - + assert current_user.id is not None plain_token = generate_embed_token() new_token = EmbedToken( user_id=current_user.id, diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 4edaff95..ab67e627 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -433,6 +433,8 @@ class UserSettingsRead(SQLModel): goal_books_per_year_enabled: bool goal_books_per_year: int gamification_enabled: bool + auto_set_date_started: bool + auto_set_date_finished: bool statistics_range: StatisticsRange statistics_custom_from: Optional[date] = None statistics_custom_to: Optional[date] = None @@ -453,10 +455,19 @@ class UserSettingsUpdate(SQLModel): goal_books_per_year_enabled: Optional[bool] = None goal_books_per_year: Optional[int] = Field(default=None, ge=1) gamification_enabled: Optional[bool] = None + auto_set_date_started: Optional[bool] = None + auto_set_date_finished: Optional[bool] = None statistics_range: Optional[StatisticsRange] = None statistics_custom_from: Optional[date] = None statistics_custom_to: Optional[date] = None + @field_validator("auto_set_date_started", "auto_set_date_finished") + @classmethod + def validate_date_automation_setting(cls, value: Optional[bool]) -> Optional[bool]: + if value is None: + raise ValueError("Reading date automation settings cannot be null") + return value + @field_validator('theme') @classmethod def validate_theme(cls, v: Optional[str]) -> Optional[str]: @@ -715,6 +726,7 @@ class DataImportPreviewRow(SQLModel): source: dict[str, Any] transformed: dict[str, Any] errors: list[str] + warnings: list[str] = Field(default_factory=list) class DataImportPreviewRequest(SQLModel): diff --git a/backend/app/services/data_import.py b/backend/app/services/data_import.py index 8e18be4a..d90817af 100644 --- a/backend/app/services/data_import.py +++ b/backend/app/services/data_import.py @@ -686,6 +686,7 @@ def preview_import( for idx, row in enumerate(rows[:limit], start=1): row_errors: list[str] = [] + row_warnings: list[str] = [] row_data = _mapped_row(row, mapping, transform_cache, {"row": idx, "total": len(rows)}, row_errors) # Validate required fields and data types for preview @@ -742,7 +743,7 @@ def preview_import( ) if reading_status == ReadingStatus.read and not date_finished: - row_errors.append( + row_warnings.append( "Marked as 'read' but has no finished date; " "without a finish date the book will not count toward monthly statistics" ) @@ -757,6 +758,7 @@ def preview_import( "source": source_display, "transformed": transformed_display, "errors": row_errors, + "warnings": row_warnings, }) return {"preview_rows": preview_rows, "row_count": len(rows), "errors": []} diff --git a/backend/tests/test_books.py b/backend/tests/test_books.py index 76b866c4..cca6d788 100644 --- a/backend/tests/test_books.py +++ b/backend/tests/test_books.py @@ -663,6 +663,96 @@ def test_update_book_sets_date_finished_when_moving_to_read(client: TestClient, assert resp.json()["date_finished"].startswith("2026-05-11T10:30:00") +def test_transition_status_respects_disabled_date_automation(client: TestClient) -> None: + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": False, "auto_set_date_finished": False}, + ) + assert settings_response.status_code == 200 + + book = _create_book(client, title="Undated transition") + reading = client.post( + f"/api/books/{book['id']}/transition-status", + json={"new_status": "currently_reading"}, + ) + assert reading.status_code == 200 + assert reading.json()["book"]["date_started"] is None + + finished = client.post( + f"/api/books/{book['id']}/transition-status", + json={"new_status": "read"}, + ) + assert finished.status_code == 200 + assert finished.json()["book"]["date_finished"] is None + + +def test_transition_status_respects_each_date_automation_setting_independently(client: TestClient) -> None: + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": False, "auto_set_date_finished": True}, + ) + assert settings_response.status_code == 200 + + book = _create_book(client, title="Independent start setting") + reading = client.post( + f"/api/books/{book['id']}/transition-status", + json={"new_status": "currently_reading"}, + ) + assert reading.status_code == 200 + assert reading.json()["book"]["date_started"] is None + + finished = client.post( + f"/api/books/{book['id']}/transition-status", + json={"new_status": "read"}, + ) + assert finished.status_code == 200 + assert finished.json()["book"]["date_finished"] is not None + + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": True, "auto_set_date_finished": False}, + ) + assert settings_response.status_code == 200 + + second_book = _create_book(client, title="Independent finish setting") + second_reading = client.post( + f"/api/books/{second_book['id']}/transition-status", + json={"new_status": "currently_reading"}, + ) + assert second_reading.status_code == 200 + assert second_reading.json()["book"]["date_started"] is not None + + second_finished = client.post( + f"/api/books/{second_book['id']}/transition-status", + json={"new_status": "read"}, + ) + assert second_finished.status_code == 200 + assert second_finished.json()["book"]["date_finished"] is None + + +def test_update_book_respects_disabled_date_automation(client: TestClient) -> None: + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": False, "auto_set_date_finished": False}, + ) + assert settings_response.status_code == 200 + + book = _create_book(client, title="Undated update") + reading = client.patch( + f"/api/books/{book['id']}", + json={"reading_status": "currently_reading"}, + ) + assert reading.status_code == 200 + assert reading.json()["date_started"] is None + + finished = client.patch( + f"/api/books/{book['id']}", + json={"reading_status": "read"}, + ) + assert finished.status_code == 200 + assert finished.json()["date_finished"] is None + + def test_update_book_does_not_override_existing_date_started(client: TestClient, monkeypatch: MonkeyPatch) -> None: book = _create_book( client, @@ -918,6 +1008,37 @@ def test_transition_status_chained_detects_started_after_finished(client: TestCl } +def test_transition_status_keeps_started_after_finished_conflict_with_auto_start_disabled( + client: TestClient, +) -> None: + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": False}, + ) + assert settings_response.status_code == 200 + + book = _create_book( + client, + title="Explicit invalid start", + reading_status="read", + date_started="2024-01-01", + date_finished="2024-02-02", + ) + response = client.post( + f"/api/books/{book['id']}/transition-status", + json={ + "new_status": "currently_reading", + "force_date_started": "2026-05-11T10:30:00Z", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["book"]["reading_status"] == "read" + assert data["book"]["date_started"] == "2024-01-01T00:00:00Z" + assert data["book"]["date_finished"] == "2024-02-02T00:00:00Z" + assert data["date_conflict"]["field"] == "started_after_finished" + + def test_transition_status_chained_option_a_clear_finished(client: TestClient, monkeypatch: MonkeyPatch) -> None: """Resolve started_after_finished with force_date_started + skip → clears date_finished.""" book = _create_book( @@ -1418,6 +1539,21 @@ def test_update_book_rejects_clearing_date_finished_for_read(client: TestClient) assert resp.json()["detail"] == "A finished book must have an end date. Change the status if you want to remove the finish date." +def test_update_book_allows_clearing_date_finished_when_automation_is_disabled(client: TestClient) -> None: + settings_response = client.patch( + "/api/profile/settings", + json={"auto_set_date_finished": False}, + ) + assert settings_response.status_code == 200 + + book = _create_book(client, title="Optional Finish Date", reading_status="read", date_finished="2024-06-01") + resp = client.patch(f"/api/books/{book['id']}", json={"date_finished": None}) + + assert resp.status_code == 200 + assert resp.json()["reading_status"] == "read" + assert resp.json()["date_finished"] is None + + def test_update_book_allows_clearing_date_finished_when_changing_status(client: TestClient) -> None: book = _create_book(client, title="Change Status", reading_status="read", date_finished="2024-06-01") diff --git a/backend/tests/test_data.py b/backend/tests/test_data.py index 2b39245d..49ff2751 100644 --- a/backend/tests/test_data.py +++ b/backend/tests/test_data.py @@ -285,6 +285,36 @@ def test_data_import_validate_and_execute_continue_on_error(client: TestClient, assert complete["failed"] == 1 +def test_data_import_preview_allows_read_book_without_finished_date( + client: TestClient, monkeypatch: MonkeyPatch, tmp_path: Path, +) -> None: + monkeypatch.setattr(settings, "import_temp_dir", str(tmp_path / "import_temp_dir")) + csv_payload = "Title,Author,Status,Availability\nDune,Frank Herbert,read,owned\n" + parse_resp = client.post( + "/api/data/import/parse", + files={"file": ("books.csv", csv_payload, "text/csv")}, + ) + file_id = parse_resp.json()["file_id"] + + preview_resp = client.post( + "/api/data/import/preview", + json={ + "file_id": file_id, + "mapping": { + "title": {"source": "Title", "transform": None}, + "author": {"source": "Author", "transform": None}, + "reading_status": {"source": "Status", "transform": None}, + "acquisition_status": {"source": "Availability", "transform": None}, + }, + }, + ) + assert preview_resp.status_code == 200 + preview = preview_resp.json() + assert preview["errors"] == [] + assert preview["preview_rows"][0]["errors"] == [] + assert any("no finished date" in warning for warning in preview["preview_rows"][0]["warnings"]) + + def test_data_import_execute_rollback_all_rolls_back(client: TestClient, monkeypatch: MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setattr(settings, "import_temp_dir", str(tmp_path / "import_temp")) csv_payload = "Title,Author\nDune,Frank Herbert\n,Missing\n" diff --git a/backend/tests/test_data_import.py b/backend/tests/test_data_import.py index 2d1f36e8..ec0b955b 100644 --- a/backend/tests/test_data_import.py +++ b/backend/tests/test_data_import.py @@ -1493,7 +1493,8 @@ def test_preview_import_read_without_finished_date( result = di.preview_import( file_id, user, {"title": ImportFieldConfig(source="title"), "reading_status": ImportFieldConfig(source="status")} ) - assert any("no finished date" in e for e in result["preview_rows"][0]["errors"]) + assert result["preview_rows"][0]["errors"] == [] + assert any("no finished date" in warning for warning in result["preview_rows"][0]["warnings"]) def test_preview_import_require_acquisition_status_invalid( diff --git a/backend/tests/test_profile.py b/backend/tests/test_profile.py index 9c48d846..02cc06f8 100644 --- a/backend/tests/test_profile.py +++ b/backend/tests/test_profile.py @@ -42,6 +42,8 @@ def test_get_settings_creates_default_when_missing(client: TestClient, session: data = resp.json() assert data["language"] == "en" assert data["user_id"] == user.id + assert data["auto_set_date_started"] is True + assert data["auto_set_date_finished"] is True def test_update_settings_creates_default_when_missing(client: TestClient, session: Session) -> None: @@ -90,6 +92,36 @@ def test_statistics_range_settings_are_persisted(client: TestClient) -> None: assert restored.json()["statistics_range"] == "custom" +def test_reading_date_automation_settings_are_persisted(client: TestClient) -> None: + response = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": False, "auto_set_date_finished": False}, + ) + assert response.status_code == 200 + data = response.json() + assert data["auto_set_date_started"] is False + assert data["auto_set_date_finished"] is False + + restored = client.get("/api/profile/settings") + assert restored.status_code == 200 + assert restored.json()["auto_set_date_started"] is False + assert restored.json()["auto_set_date_finished"] is False + + partial = client.patch( + "/api/profile/settings", + json={"auto_set_date_started": True}, + ) + assert partial.status_code == 200 + assert partial.json()["auto_set_date_started"] is True + assert partial.json()["auto_set_date_finished"] is False + + null_value = client.patch( + "/api/profile/settings", + json={"auto_set_date_finished": None}, + ) + assert null_value.status_code == 422 + + def test_statistics_range_settings_reject_invalid_dates(client: TestClient) -> None: response = client.patch( "/api/profile/settings", diff --git a/docs/guide/database-layout.md b/docs/guide/database-layout.md index f69d0f6c..ca9b3ade 100644 --- a/docs/guide/database-layout.md +++ b/docs/guide/database-layout.md @@ -298,6 +298,8 @@ Per-user settings such as language, timezone, and theme. | `timezone` | `VARCHAR(64)` | NOT NULL | default `UTC` | | `theme` | `VARCHAR(20)` | NOT NULL | default `light` | | `custom_theme` | `VARCHAR(30)` | | | +| `auto_set_date_started` | `BOOLEAN` | NOT NULL | default `true` | +| `auto_set_date_finished` | `BOOLEAN` | NOT NULL | default `true` | ### `book_author` diff --git a/docs/guide/using-librislog/profile.md b/docs/guide/using-librislog/profile.md index 0006c039..8cbdd7ae 100644 --- a/docs/guide/using-librislog/profile.md +++ b/docs/guide/using-librislog/profile.md @@ -43,6 +43,21 @@ The **"Show reading streaks & goals on dashboard"** switch above the goals disab See [Dashboard → Reading Streaks & Goals](/guide/using-librislog/dashboard#reading-streaks-goals) for details on how streaks and goal progress are calculated. +## Reading Date Automation + +By default, LibrisLog fills in missing reading dates when you change a book's status: + +- Moving a book to **Currently Reading** sets its start date to the current date and time if no start date exists. +- Moving a book to **Read** or **Did Not Finish** sets its finish date to the current date and time if no finish date exists. + +Existing dates entered by you are preserved and are never overwritten automatically. + +You can disable these behaviors independently in the **Reading Date Automation** section of your profile. When disabled, the status still changes, but the corresponding date remains empty. This is useful when you know a book's status but do not know when you started or finished it. + +Books without the relevant dates are omitted from date-based views and calculations, such as the reading timeline, finished-books-by-month charts, reading duration, and date-based goals. They remain included in status-based totals, such as the number of books marked **Read**. + +Disabling automation does not remove existing dates or disable date-conflict protection. It only prevents LibrisLog from creating a missing date automatically. + ## URL/Profile Sharing Create a read-only public view of your reading profile and share it with a URL. The shared page does not expose editing controls, notes, blurbs, or your email address. diff --git a/frontend/src/lib/components/BookDrawer.svelte b/frontend/src/lib/components/BookDrawer.svelte index 202a6367..c28b70dd 100644 --- a/frontend/src/lib/components/BookDrawer.svelte +++ b/frontend/src/lib/components/BookDrawer.svelte @@ -1,10 +1,12 @@