Add client media upload and cache pipeline#16
Merged
Conversation
…ndling - Implemented BookDownloadService for downloading book files. - Added retry mechanism for failed media uploads in MediaUploadQueue. - Enhanced BookDetailsPage to include download option in context menu. - Created deleteBookWithMediaCleanup service to handle book deletion and media upload cleanup. - Updated ProfilePage to display failed media uploads and provide retry option. - Added tests for new download and delete functionalities, ensuring proper behavior.
There was a problem hiding this comment.
Pull request overview
This PR adds an authenticated media pipeline to the Papyrus Flutter client: scoped media upload queueing (per server profile + user), authenticated media API integration (usage/upload/download/delete), and a stable local cover rendering path that reuses Flutter’s ImageCache while persisting bytes in platform-local storage (native filesystem / web OPFS).
Changes:
- Add scoped authenticated media support (models, API client/repository methods, upload queue + processing hooks, and storage usage UI).
- Implement private cover rendering via a stable
ImageProvider, plus consistent book-cover surfaces that supportcoverMediaId. - Add local book-file caching + download flows and deterministic import/delete commit/cleanup services with extensive tests.
Reviewed changes
Copilot reviewed 83 out of 84 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/superpowers/specs/2026-07-11-media-storage-hardening-design.md | New design spec for scoped media storage, upload queue, and cover caching behavior. |
| docs/superpowers/specs/2026-07-11-cover-image-cache-design.md | New design spec for stable ImageProvider identity for local covers. |
| docs/superpowers/plans/2026-07-11-stable-local-cover-image-cache.md | Implementation plan for stable cover cache provider + widget integration. |
| app/web/book_worker.js | Add OPFS handlers for storing book files and cover CRUD/promotion/clearing with scoped locking. |
| app/pubspec.yaml | Add http_parser as a direct dependency for multipart content types. |
| app/pubspec.lock | Lockfile update for http_parser direct dependency. |
| app/lib/widgets/topics/manage_topics_sheet.dart | Use PrivateBookCover for consistent private/public cover rendering. |
| app/lib/widgets/shelves/shelf_card.dart | Use PrivateBookCover in shelf mosaic previews (supports private covers). |
| app/lib/widgets/shelves/move_to_shelf_sheet.dart | Use PrivateBookCover for cover rendering in the move-to-shelf sheet. |
| app/lib/widgets/shared/book_group_header.dart | Add bookId/coverMediaId plumbing and render via PrivateBookCover. |
| app/lib/widgets/library/book_list_item.dart | Replace network-image rendering with PrivateBookCover. |
| app/lib/widgets/library/book_card.dart | Replace network-image rendering with PrivateBookCover. |
| app/lib/widgets/dashboard/recently_added_section.dart | Render dashboard covers via PrivateBookCover. |
| app/lib/widgets/dashboard/continue_reading_card.dart | Render the continue-reading cover via PrivateBookCover. |
| app/lib/widgets/context_menu/book_context_menu.dart | Add Download action + align labels (Download/Delete) and cover rendering. |
| app/lib/widgets/book/private_book_cover.dart | New unified cover widget: public URL / inline data / private cached media / pending / guest. |
| app/lib/widgets/book_details/book_header.dart | Pass bookId + coverMediaId through to details header cover. |
| app/lib/widgets/book_details/book_cover_image.dart | Route cover rendering through PrivateBookCover. |
| app/lib/widgets/add_book/import_book_sheet.dart | Move import commit to a deterministic commit boundary + enqueue scoped uploads. |
| app/lib/utils/bulk_book_actions.dart | Bulk delete now removes pending uploads and best-effort local media cleanup. |
| app/lib/utils/book_actions.dart | Implement Download + Delete actions using media cache + cleanup services. |
| app/lib/services/book_import_service.dart | Web worker integration extended for OPFS book caching + scoped cover storage requests. |
| app/lib/services/book_import_service_stub.dart | Native implementation for book file storage + scoped cover store/read/delete/promote/clear. |
| app/lib/services/book_import_commit_service.dart | New service to commit imports with ordered persistence + compensation. |
| app/lib/services/book_download_service.dart | New cross-platform book-file download service + safe filenames. |
| app/lib/services/book_download_service_platform_web.dart | Web download implementation via Blob + anchor download. |
| app/lib/services/book_download_service_platform_io.dart | IO download implementation via FilePicker save + file write when needed. |
| app/lib/services/book_delete_cleanup_service.dart | New helper to delete a book with pending-uploads + local media cleanup. |
| app/lib/providers/sync_settings_provider.dart | Support quota override in storage label formatting. |
| app/lib/providers/notes_provider.dart | Expose coverMediaId for grouped book headers. |
| app/lib/providers/bookmarks_provider.dart | Expose coverMediaId for grouped book headers. |
| app/lib/providers/annotations_provider.dart | Expose coverMediaId for grouped book headers. |
| app/lib/providers/auth_provider.dart | Expose authenticated downloadMedia method. |
| app/lib/powersync/sync_profile_switch_queue.dart | New serializing queue for profile switches that preserves latest request. |
| app/lib/powersync/storage_sync_controller.dart | Display server media usage + failed media upload count/label. |
| app/lib/powersync/powersync_book_mapper.dart | Map new file_media_id / cover_media_id fields through PowerSync. |
| app/lib/powersync/papyrus_schema.dart | Add file_media_id / cover_media_id columns to schema. |
| app/lib/powersync/papyrus_powersync_connector.dart | Add onUploadComplete hook to trigger media-upload processing. |
| app/lib/pages/profile_page.dart | Surface media usage + failed upload retry in Storage & sync UI; clear cover cache on clear-auth-cache. |
| app/lib/pages/notes_page.dart | Pass bookId + coverMediaId to BookGroupHeader. |
| app/lib/pages/bookmarks_page.dart | Pass bookId + coverMediaId to BookGroupHeader. |
| app/lib/pages/annotations_page.dart | Pass bookId + coverMediaId to BookGroupHeader. |
| app/lib/pages/book_details_page.dart | Add Download/Delete actions; ensure book file is cached before reading when needed. |
| app/lib/models/shelf.dart | Extend CoverPreview to include bookId and mediaId. |
| app/lib/models/book.dart | Add fileMediaId / coverMediaId fields to book model + JSON/copyWith. |
| app/lib/media/media_upload_queue.dart | New scoped, persisted media upload queue with single-flight processing and retries. |
| app/lib/media/media_storage_scope.dart | New media scope identity (profile + user) with safe path parts. |
| app/lib/media/media_models.dart | New media models: kinds, payload, asset, usage, and upload exception. |
| app/lib/media/media_cache_service.dart | New lazy download + validation + caching for private book files and covers. |
| app/lib/media/local_cover_image_provider.dart | New filesystem-backed ImageProvider key for stable Flutter image cache identity. |
| app/lib/media/cover_upload_persistence.dart | New helper to promote pending covers after successful upload (best-effort). |
| app/lib/media/cover_storage_bucket.dart | New enum describing cover storage buckets (cached/pending/guest books). |
| app/lib/main.dart | Wire up scope activation, media queue processing, media usage refresh, and services/providers. |
| app/lib/data/data_store.dart | Add repository-bound awaitable mutations + preserve local cover media id through transient sync snapshots. |
| app/lib/auth/auth_repository.dart | Add authenticated media methods (usage/upload/download/delete). |
| app/lib/auth/auth_api_client.dart | Implement /v1/media client calls: usage, multipart upload, download, delete. |
| app/test/widgets/shared/book_group_header_test.dart | New test verifying BookGroupHeader forwards bookId to cover renderer. |
| app/test/widgets/library/book_list_item_test.dart | Add test verifying bookId forwarding to PrivateBookCover. |
| app/test/widgets/library/book_card_test.dart | Add test verifying bookId forwarding to PrivateBookCover. |
| app/test/widgets/context_menu/book_context_menu_test.dart | New test for Download/Delete label/order and download callback. |
| app/test/widgets/book_details/book_cover_image_test.dart | New test verifying BookCoverImage forwards bookId to PrivateBookCover. |
| app/test/services/book_import_service_test.dart | Add tests for storing downloaded book bytes with correct extension + replacement. |
| app/test/services/book_import_commit_service_test.dart | New tests for import commit ordering, scoping, compensation, and context guards. |
| app/test/services/book_delete_cleanup_service_test.dart | New tests for delete cleanup ordering and coverage of cover representations. |
| app/test/powersync/sync_profile_switch_queue_test.dart | New tests for profile-switch queue serialization and error handling. |
| app/test/powersync/storage_sync_controller_test.dart | New test for failed media upload label exposure in UI controller. |
| app/test/powersync/powersync_book_mapper_test.dart | Extend mapper tests for new media-id fields. |
| app/test/pages/profile_storage_sync_test.dart | Update profile storage/sync page harness to provide media queue and adjust pumps. |
| app/test/pages/book_details_delete_test.dart | New widget tests for delete cleanup + download behavior + menu ordering. |
| app/test/media/media_storage_scope_test.dart | New tests for scope identity, equality, and path safety. |
| app/test/media/media_profile_switch_contract_test.dart | New contract tests that assert key lifecycle wiring in main.dart/import sheet. |
| app/test/media/media_cache_service_test.dart | New tests for lazy cache/download behavior + cover download coalescing. |
| app/test/media/local_cover_image_provider_test.dart | New tests for stable provider identity reuse + eviction semantics. |
| app/test/media/cover_upload_persistence_test.dart | New tests for upload/promotion best-effort behavior. |
| app/test/data/book_repository_data_store_test.dart | Extend DataStore tests for new async mutation semantics and media-id retention. |
| app/test/auth/auth_api_client_test.dart | Add tests for media usage, download, and multipart upload request mapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+635
to
641
| void _onMenuAction(String action) async { | ||
| switch (action) { | ||
| case 'download': | ||
| await _onDownloadBookFile(); | ||
| case 'delete': | ||
| // TODO: Confirm and delete | ||
| ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Delete functionality coming soon'))); | ||
| await _confirmDeleteBook(); | ||
| } |
Comment on lines
+245
to
+255
| final actualBytes = bytes.offsetInBytes == 0 && bytes.lengthInBytes == bytes.buffer.lengthInBytes | ||
| ? bytes | ||
| : Uint8List.fromList(bytes); | ||
| final jsBuffer = actualBytes.buffer.toJS; | ||
| final message = JSObject(); | ||
| message['type'] = 'storeFile'.toJS; | ||
| message['format'] = normalizedExtension.toJS; | ||
| message['bookId'] = bookId.toJS; | ||
| message['fileData'] = jsBuffer; | ||
|
|
||
| worker.postMessage(message, [jsBuffer].toJS); |
Comment on lines
+159
to
+170
| final mergedBooks = books | ||
| .map((book) { | ||
| final localBook = _books[book.id]; | ||
| if (book.coverMediaId == null && localBook?.coverMediaId != null) { | ||
| // PowerSync can briefly emit the downloaded server row before its | ||
| // pending local media-reference update is acknowledged. Keep the | ||
| // established local reference through that transient null snapshot. | ||
| return book.copyWith(coverMediaId: localBook!.coverMediaId); | ||
| } | ||
| return book; | ||
| }) | ||
| .toList(growable: false); |
Comment on lines
153
to
158
| onStatusChange?.call(ReadingStatus.inProgress); | ||
| case 'finished': | ||
| onStatusChange?.call(ReadingStatus.completed); | ||
| case 'download': | ||
| onDownload?.call(); | ||
| case 'delete': |
… handle loading state
…te related providers for consistent state management
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.
Summary
Depends on
Verification