Skip to content

Add client media upload and cache pipeline#16

Merged
Eoic merged 48 commits into
masterfrom
codex/media-storage-pipeline
Jul 12, 2026
Merged

Add client media upload and cache pipeline#16
Eoic merged 48 commits into
masterfrom
codex/media-storage-pipeline

Conversation

@Eoic

@Eoic Eoic commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Add authenticated media API methods for usage, upload, download, and delete
  • Add book file/cover media IDs to the local sync model and upload mapper
  • Queue media uploads after signed-in imports and retry after sync/upload completion
  • Show real server file-storage usage in Storage & sync
  • Lazy-load private uploaded covers and cache downloaded book files in platform-local storage

Depends on

Verification

  • flutter analyze
  • flutter test

Eoic added 28 commits July 5, 2026 01:16
…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.
@Eoic Eoic marked this pull request as ready for review July 12, 2026 12:53
Copilot AI review requested due to automatic review settings July 12, 2026 12:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 support coverMediaId.
  • 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':
@Eoic Eoic merged commit 039651c into master Jul 12, 2026
1 check passed
@Eoic Eoic deleted the codex/media-storage-pipeline branch July 12, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants