Complete offline library sync and fix annotation editing and favorites - #23
Merged
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
It expands the sync schema and persistence flow across many entities/UI paths, and the remaining issues (snapshot trigger query cost and favorite-toggle consistency) warrant careful human review before merging.
Pull request overview
This PR advances Papyrus’s local-first “offline library” by expanding the synced PowerSync schema beyond books (shelves/topics/notes/annotations/bookmarks/memberships), adding typed repositories + snapshot watching, and updating editors/UI actions to await persistence (or remain open on failure) so users don’t lose changes while offline or during profile switches.
Changes:
- Expand PowerSync schema + mapping to support multi-entity library sync, legacy metadata promotion, and explicit-null handling.
- Introduce persistent editor/save behavior across sheets/dialogs (await writes, keep UI open on failure, retry-friendly).
- Fix/adjust UX flows for annotation editing, physical-book bookmark persistence, and favorites persistence.
File summaries
| File | Description |
|---|---|
| docs/superpowers/specs/2026-08-23-book-storage-status-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-23-book-import-sheet-refactor-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-23-book-import-drop-zone-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-01-shelves-page-controls-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-01-shelf-page-heading-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-01-reusable-shelf-books-page-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-01-book-import-workflow-rework-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-08-01-advanced-filter-sheet-sizing-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-30-advanced-library-filters-visual-redesign-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-27-reader-integration-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-24-acquisition-bottom-sheets-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-14-import-add-loading-state-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-14-import-action-button-shapes-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-14-add-book-backdrop-continuity-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-13-book-edit-responsive-pane-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-13-add-book-bottom-sheets-design.md | Remove obsolete spec document. |
| docs/superpowers/specs/2026-07-11-cover-image-cache-design.md | Remove obsolete spec document. |
| docs/superpowers/plans/2026-08-23-book-import-drop-zone.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-08-01-shelf-page-heading.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-08-01-advanced-filter-sheet-sizing.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-27-reader-integration.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-14-import-add-loading-state.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-14-import-action-button-shapes.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-14-add-book-backdrop-continuity.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-13-book-edit-responsive-pane.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-13-add-book-bottom-sheets.md | Remove obsolete plan document. |
| docs/superpowers/plans/2026-07-12-book-edit-layout-alignment.md | Remove obsolete plan document. |
| app/test/widgets/persistent_editor_test.dart | Add widget coverage ensuring editors await persistence and remain open on failure. |
| app/test/widgets/book_details/bookmark_dialog_test.dart | Add widget coverage for bookmark create/edit/delete + persistence waiting. |
| app/test/providers/library_favorite_persistence_test.dart | Add provider+PowerSync tests for favorite persistence, scope, and remote changes. |
| app/test/providers/bookmark_persistence_test.dart | Add provider tests for bookmark merges + stale-handle/profile-switch isolation. |
| app/test/providers/book_edit_persistence_test.dart | Add provider tests for stale editor save behavior and scope isolation. |
| app/test/providers/book_details_provider_test.dart | Update tests to await async CRUD and ensure remote note-only changes notify. |
| app/test/powersync/powersync_book_mapper_test.dart | Extend mapper tests for promoted fields, null semantics, cover URL filtering. |
| app/test/powersync/papyrus_schema_mode_test.dart | Update schema tests for multi-table library schemas (guest localOnly, account synced). |
| app/test/powersync/bookmark_persistence_test.dart | Add PowerSync persistence/merge/isolation tests for bookmarks. |
| app/test/pages/annotation_edit_test.dart | Add widget test ensuring full annotation editor opens prefilled and saves all fields. |
| app/test/models/shelf_icon_test.dart | Add tests for shelf icon descriptor round-tripping + unsupported icon identity preservation. |
| app/test/models/library_nullable_fields_test.dart | Add tests for explicit clearing of nullable fields across models. |
| app/pubspec.yaml | Make sqlite_async a direct dependency. |
| app/pubspec.lock | Record sqlite_async as direct main dependency. |
| app/lib/widgets/topics/topic_detail_sheet.dart | Make topic edit/delete await repository writes and show error feedback on delete failure. |
| app/lib/widgets/topics/manage_topics_sheet.dart | Await saves, use persistent-save behavior, and UUID ids for new topics. |
| app/lib/widgets/topics/add_topic_sheet.dart | Make onSave async-capable and keep sheet open until persistence succeeds. |
| app/lib/widgets/shelves/move_to_shelf_sheet.dart | Await saves, use persistent-save behavior, and UUID ids for new shelves. |
| app/lib/widgets/shelves/add_shelf_sheet.dart | Make onSave async-capable and keep sheet open until persistence succeeds. |
| app/lib/widgets/shared/persistent_save.dart | Introduce shared “persist before dismiss” mixin with generic failure snackbar. |
| app/lib/widgets/library/book_grid.dart | Route favorite toggles through shared favorite persistence helper. |
| app/lib/widgets/bookmarks/bookmark_action_sheet.dart | Make bookmark note/color/delete await persistence; keep UI open on failures. |
| app/lib/widgets/book/book_details.dart | Persist memberships via centralized updateBookMemberships rather than manual add/remove loops. |
| app/lib/widgets/book_details/note_dialog.dart | Make note save await persistence and use UUID ids; disable actions while saving. |
| app/lib/widgets/book_details/bookmark_dialog.dart | Make bookmark save await persistence and use UUID ids; disable actions while saving. |
| app/lib/widgets/book_details/annotation_dialog.dart | Make annotation save await persistence and use UUID ids; preserve immutable location fields when editing. |
| app/lib/widgets/book_details/annotation_action_sheet.dart | Rename “Edit note” to “Edit annotation” and adjust action enum. |
| app/lib/widgets/annotations/annotation_action_sheet.dart | Make annotation note editing await persistence and keep sheet open on failure. |
| app/lib/utils/bulk_book_actions.dart | Persist bulk shelf/topic membership changes via repository writer; make bulk favorite toggle awaited + error surfaced. |
| app/lib/utils/book_actions.dart | Add toggleBookFavorite helper with awaited persistence + error snackbar; use it from context menu. |
| app/lib/providers/shelves_provider.dart | Switch shelf CRUD + reorder to async persistence with repository support; use UUIDs. |
| app/lib/providers/notes_provider.dart | Make note CRUD async and repository-aware; pin toggles now await persistence. |
| app/lib/providers/library_provider.dart | Make favorites persistence async; optionally source favorite state directly from DataStore. |
| app/lib/providers/bookmarks_provider.dart | Make bookmark CRUD async + repository-aware with “previous” support. |
| app/lib/providers/book_edit_provider.dart | Save through updateBookAndWait with captured repository handle to avoid stale writes. |
| app/lib/providers/book_details_provider.dart | Make details CRUD async + repository-aware; always notify on DataStore changes for child-record updates. |
| app/lib/providers/annotations_provider.dart | Make annotation CRUD async + repository-aware with explicit-null clearing. |
| app/lib/powersync/powersync_service.dart | Implement LibraryRepository, watch multi-table library snapshots, and clear guest library across all library tables. |
| app/lib/powersync/powersync_book_mapper.dart | Promote additional book fields into synced columns; preserve explicit null/absent fields; strengthen remote cover URL filtering. |
| app/lib/powersync/papyrus_schema.dart | Expand PowerSync schemas to include shelves/tags/notes/annotations/bookmarks/membership tables (+ local migrations table). |
| app/lib/powersync/papyrus_powersync_connector.dart | Encode non-book CRUD payloads via shared library row decoding. |
| app/lib/powersync/library_row_mapper.dart | Add shared row encode/decode + per-entity mappers and library table list. |
| app/lib/pages/shelves_page.dart | Await shelf create/edit/delete persistence and surface delete failures via snackbar. |
| app/lib/pages/shelf_contents_page.dart | Await shelf edit persistence with repository + previous value supplied. |
| app/lib/pages/notes_page.dart | Route note edits through persistence-aware dialog callback; await delete and surface failures. |
| app/lib/pages/bookmarks_page.dart | Route bookmark note/color/delete through persistence-aware callbacks; await deletes. |
| app/lib/pages/book_details_page.dart | Route note/bookmark/annotation CRUD through persistence-aware dialogs; make annotation editing open full editor. |
| app/lib/pages/annotations_page.dart | Route annotation edit through full editor; await deletes and surface failures. |
| app/lib/models/tag.dart | Support explicit clearing of nullable description in copyWith. |
| app/lib/models/shelf.dart | Add ShelfIconDescriptor for stable identity + explicit nullable clears; preserve unsupported icon descriptors. |
| app/lib/models/note.dart | Support explicit clearing of nullable location in copyWith. |
| app/lib/models/book.dart | Support explicit clearing of additional nullable metadata fields in copyWith. |
| app/lib/models/annotation.dart | Support explicit clearing of nullable note in copyWith. |
| app/lib/main.dart | Provide DataStore to LibraryProvider so favorites can persist through the repository. |
| app/lib/data/repositories/library_repository.dart | Introduce typed repository interfaces for non-book entities and membership writes. |
Review details
- Files reviewed: 95/96 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
282
to
+284
| _booksSubscription = database | ||
| .watch('SELECT * FROM books ORDER BY added_at DESC', triggerOnTables: ['books']) | ||
| .listen((rows) { | ||
| _booksController.add(rows.map((row) => PowerSyncBookMapper.fromRow(Map<String, Object?>.from(row))).toList()); | ||
| }); | ||
| .watch('SELECT count(*) FROM books', triggerOnTables: libraryTableNames) | ||
| .asyncMap((_) => library.snapshot()) |
Comment on lines
+76
to
+86
| Future<void> toggleBookFavorite(BuildContext context, String bookId, bool currentFavorite) async { | ||
| try { | ||
| await context.read<LibraryProvider>().toggleFavorite(bookId, currentFavorite); | ||
| } catch (_) { | ||
| if (context.mounted) { | ||
| ScaffoldMessenger.of( | ||
| context, | ||
| ).showSnackBar(const SnackBar(content: Text('Could not save favorite. Please try again.'))); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
No linked ticket.
Summary
Library changes now persist locally and synchronize between devices for books, shelves, topics, notes, annotations, bookmarks, and memberships. Guest data remains local, while account/server switches clear prior library views and invalidate stale editor handles.
Testing
master, using installed Flutter 3.44.8. The target branch's SDK-pinned lockfile entries are preserved.PAPYRUS_LIVE_SYNC=1 flutter test test/powersync/library_live_sync_test.dart --reporter expandedfromapp/.UI evidence
Widget tests cover the annotation menu opening the full prefilled editor and saving every editable field, plus delayed/failed bookmark saves. The requester confirmed the fixes work during manual testing.
Compatibility and migration
Depends on PapyrusReader/server#5. Deploy the server migrations/upload handlers, refresh publication/grants, and activate the expanded sync configuration before releasing this client. No reset, key regeneration, or automatic guest import is required.
sqlite_asyncis now declared directly at its existing locked version.Notes
Reading sessions/goals, standalone series management, guest import, conflict-resolution UI, and expanded file-transfer behavior remain outside this change. Existing series descriptors on books synchronize. Latest
masterplatform/build fixes are included.Workspace integration: PapyrusReader/papyrus#1.
Checklist