summaryrefslogtreecommitdiff
path: root/player-android
AgeCommit message (Collapse)Author
2026-05-23Configure audio_service notification icon so playback does not crashmainPaul Buetow
Android refuses to post a foreground-service notification without a small-icon drawable; without androidNotificationIcon set, the very first AudioPlayer.play() triggers a FATAL "Invalid notification (no valid small icon)" the moment audio_service tries to publish the media-session metadata, killing the app process. Re-use drawable/ic_launcher — the project's existing launcher icon — as the small notification icon. A dedicated monochrome silhouette would be more polished, but the launcher icon is good enough to unblock playback today. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Pass session cookie to ExoPlayer/just_audio for authenticated streamingPaul Buetow
The audio/video players spawn a localhost proxy (just_audio) or call ExoPlayer directly (video_player) using their own HTTP stack, which does not share Dio's cookie jar. Without the session cookie those requests hit the stream endpoint anonymously and fail with 401. Expose the Dio CookieJar via a Riverpod provider (cookieJarProvider) and attach a Cookie header (alongside the existing Authorization: Bearer) to both AudioSource.uri and VideoPlayerController.networkUrl. Also enable android:usesCleartextTraffic="true" on the Application — just_audio's headers-injection proxy listens on 127.0.0.1 and Android 28+ blocks cleartext to it without the explicit opt-in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Add migration for missing playback_progress.finished column (5f)Paul Buetow
Existing databases created before the finished column was added to the base schema still satisfied CREATE TABLE IF NOT EXISTS and never got the column, causing GET /api/v1/media/{id} to fail with "SQL logic error: no such column: finished" and break media detail view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix review issues for API token management screen (task hb)Paul Buetow
- Make `Key('api_tokens_copy_snackbar')` const (promoted by outer const SnackBar) - Fix misleading comment in _revokeToken: mirrors AdminUsersScreen (append on revert), not MySharesScreen (which uses index-based re-insert) - Add widget test: submits null expiresInDays when no expiry date selected - Add unit tests for expiresInDays clamp logic (correct days, min 1, max 36500) - Skip 403 handling in apiTokenErrorMessage: token endpoints use requireSession middleware and the service layer never returns ErrForbidden for token ops Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement API token management screen in Settings (task hb)Paul Buetow
Adds ApiTokensScreen (/settings/api-tokens) so any authenticated user can list, create (with optional expiry), and revoke their own Bearer API tokens. The plaintext token is displayed exactly once after creation with a clipboard copy button. Create and revoke use optimistic UI with proper revert on error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Remove dead index parameter from _restore and _hardDelete in admin_trash_screenPaul Buetow
The index was never used after identity-based removeWhere replaced index-based removal. Drop it from the method signatures, the _TrashList/_TrashTile callback types, and all call sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix review issues and add widget tests for admin screens (eb)Paul Buetow
- Fix typo _isTriggerring → _isTriggering in AdminRescanScreen - Replace index-based removeAt with identity-based removeWhere in AdminTrashScreen - Add .cast<T>() for list results from Future.wait in AdminPermissionsScreen - Add tap-outside safety comment in _confirmHardDelete - Fix broken dartdoc reference [RescanScreen] → [AdminRescanScreen] - Add widget tests for AdminRescanScreen, AdminTrashScreen, AdminPermissionsScreen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement AdminPermissionsScreen, RescanScreen, and TrashScreen (eb)Paul Buetow
Add three admin-only screens with routes and navigation tiles: - AdminPermissionsScreen: permission matrix (users × sets) with optimistic grant/revoke checkboxes; admin rows shown as disabled (implicit access). - AdminRescanScreen: trigger library rescan, poll getScanProgress every 2s while running, cancel timer in dispose, show live file/set counters. - AdminTrashScreen: list soft-deleted media with restore + hard-delete (confirmation dialog); optimistic UI with revert-on-error for both actions. All three use generation counters for stale-async cancellation and guard async continuations on mounted. Added error mappers and route constants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix final review issues for AdminUsersScreen (task db)Paul Buetow
- Add bounds check on optimistic create success path to mirror the error path guard - Refactor _CreateUserDialogState.build() by extracting _buildUsernameField() and _buildPasswordField() helpers; remove stale line-count comment - Extract _AdminSection ConsumerWidget from SettingsScreen.build() following the _ThemeToggle pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix review issues in AdminUsersScreen and related files (task db)Paul Buetow
- Replace identity-equality optimistic-create revert with index-based logic (placeholderIdx) to avoid relying on reference equality - Replace insert(index) delete revert with append to avoid stale-index position jitter from concurrent mutations - Remove _RoleBadge key collision; update test to use text finders - currentUserProvider catch block returns null instead of User(id:0) to avoid colliding with the optimistic placeholder sentinel; fix broken comment - adminUserErrorMessage 400-branch delegates to dioErrorMessage to eliminate duplicated JSON body-parsing logic - Add Completer-backed optimistic placeholder visibility test - _EmptyView: replace magic height SizedBox with LayoutBuilder+Center - _CreateUserDialogState: inline _buildForm and _buildActions into build() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement AdminUsersScreen with create/delete user and admin gating (db)Paul Buetow
- Add AdminUsersScreen with user list, create dialog, delete confirmation, and optimistic UI (revert on error) for both create and delete operations. - Add currentUserProvider (FutureProvider) to resolve the logged-in User object from token storage + listUsers, used for self-delete gating and Settings admin section visibility. - Gate Admin section (Manage Users tile) in SettingsScreen behind currentUserProvider → isAdmin, providing defence-in-depth alongside server-side 403 enforcement. - Add adminUserErrorMessage to error_mappers.dart with 400/403/409 handling. - Add adminUsers route constant (AppRoutes.adminUsers) and GoRouter entry. - Add 25 tests in admin_users_screen_test.dart and 3 admin-section tests in settings_screen_test.dart (397 tests total pass). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix gb final review: SizedBox.shrink footer and shares_no_more testPaul Buetow
Replace Text('') with SizedBox.shrink() in _buildFooter so dead space and the premature episodes_no_more key are eliminated when more pages exist. Add testWidgets for the shares_no_more footer key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix gb review issues: _isLoadingMore stuck on refresh, empty footer space, ↵Paul Buetow
pagination tests - Reset _isLoadingMore = false inside _load()'s setState in both MediaGridScreen and PodcastEpisodesScreen, so a generation-mismatch early return in an in-flight _loadMore does not leave the spinner permanently stuck after a pull-to-refresh (major bug fix). - Replace the empty Text('') with SizedBox.shrink() in _buildFooterSliver when hasMore=true and isLoadingMore=false to eliminate the 32px dead space (nit fix). - Add three pagination widget tests to each screen: _loadMore appends a second page, _loadMore is a no-op while already in-flight, and pull-to-refresh while _loadMore is in-flight leaves _isLoadingMore=false after _load completes (regression coverage for the major bug). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement infinite-scroll pagination on MediaGridScreen, episode list, and ↵Paul Buetow
my shares - MediaGridScreen: adds ScrollController+CustomScrollView; limit/offset params (page size 50); generation counter guards stale _loadMore results; shows CircularProgressIndicator at bottom while loading, "No more items" when done. - PodcastEpisodesScreen: adds NotificationListener<ScrollNotification> outside RefreshIndicator (avoids ListView+controller interference with overscroll); limit/offset (page size 50); same generation counter pattern; shows footer spinner or "All episodes loaded" message. - MySharesScreen: adds end-of-list "All shares loaded" indicator after first successful fetch (shares API returns all items in a single response, no server-side pagination available). - Pull-to-refresh resets offset=0 and hasMore=true on all three screens. - All 362 existing tests pass; flutter analyze reports no issues. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Add widget test for double-tap download guard in PodcastEpisodesScreenPaul Buetow
Adds a Completer<Media> field to _FakeApiClient so tests can hold a download in-flight, and a new test that taps the download button twice while the first call is pending to verify _pendingDownloads suppresses the second API call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix bb review issues: stale snapshot, double-tap guard, button dedup, commentsPaul Buetow
- _downloadEpisodeAt: read _episodes fresh inside setState instead of using the pre-await snapshot, and guard that the index is still valid and the row still lacks a mediaId, preventing silent overwrites of data refreshed by _load() during the await. - Add Set<int> _pendingDownloads to prevent concurrent download API calls when the user double-taps; visually disable the button while in-flight via new isLoading parameter on _DownloadButton. - Extract _EpisodeActionButton shared primitive to eliminate structural duplication between _PlayButton and _DownloadButton (DRY). - Fix misleading mediaId null-safety comment to describe the actual guarantee. - Add comments to episodeToggleErrorMessage and episodeDownloadErrorMessage explaining why they use action-specific fallback strings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Add play and download buttons to PodcastEpisodesScreen (bb)Paul Buetow
Each episode row now shows a play button (when mediaId is non-null) that navigates to AudioPlayerScreen, or a download button (when mediaId is null) that triggers a server-side download via downloadEpisode. On success the row updates in-place to swap the download button for a play button without requiring a full page reload. Added episodeDownloadErrorMessage to error_mappers.dart and extended the test suite to cover all new paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix final review nits for Material 3 theming (task fb)Paul Buetow
- Remove redundant multiSelectionEnabled: false from SegmentedButton - DRY up theme toggle tests via optional themeNotifier param in _pumpSettingsScreen - Add UI assertion to verify button reflects ThemeMode.dark after tap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Fix Material 3 theming review issues (task fb)Paul Buetow
- Roll back ThemeNotifier state on SharedPreferences write failure so in-memory and disk never diverge - Fix stale comment: scaffoldBackgroundColor ← --bg-body (not background) - Fix source attribution: theme.css not docs/theming.md - Make _ThemeToggle const-constructible; remove incorrect ignore comment - Inline _buildSegmentedButton into _ThemeToggle.build (was a trivial passthrough) - Add defensive isNotEmpty comment on SegmentedButton.onSelectionChanged - Hoist buildLightTheme()/buildDarkTheme() to module-level finals in main.dart so ThemeData is built once at startup rather than on every rebuild - Add _ThemeToggle tests: initial segment selection and segment tap dispatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement Material 3 theming with light + dark mode toggle (fb)Paul Buetow
Adds ThemeNotifier (AsyncNotifier) backed by shared_preferences to persist the user's light/dark/system preference across restarts. Color schemes derive from the dark and light palettes defined in player-server/docs/theming.md. MaterialApp now consumes themeProvider for themeMode, theme, and darkTheme; SettingsScreen gains a SegmentedButton appearance section for the toggle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22Implement public ShareViewerScreen with Android deep-link intent-filter (9b)Paul Buetow
- New ShareViewerScreen (/share/:token): unauthenticated share-viewer that fetches share metadata via publicApiClientProvider, renders filename, type, duration, thumbnail, and a Play button routing to the video/audio player. - New publicApiClientProvider: bare Dio client (no auth interceptors) for the public share endpoint; shares kPlayerBaseUrl with the authenticated client. - AndroidManifest: http + https deep-link intent-filters for /share/.* so Android routes share URLs directly into the app (App Links / autoVerify). - router.dart: /share/:token bypasses the authentication redirect; guard uses AppRoutes.shareViewerPrefix constant instead of a raw '/share/' string (DIP). - app_routes.dart: shareViewer route constant, shareViewerPrefix, shareViewerPath helper. - error_mappers.dart: shareViewerErrorMessage — 404 invalid/revoked, 410 expired. - player_api_client.dart: baseUrl getter encapsulates rawDio.options.baseUrl so screens never access transport internals directly (ISP, DIP). - Review fixes: OCP icon map in _FallbackThumbnail, LSP explicit baseUrl overrides in test fakes, DIP shareViewerPrefix constant. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement offline progress queue with sqflite and connectivity_plus (3b)Paul Buetow
Introduces ProgressQueue backed by SQLite for offline-capable progress tracking, with ProgressQueueBase (LSP+DIP), databaseFactory injection (DIP), and ProgressSyncClient narrow interface (ISP). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement podcast episode played/unplayed toggle with progress bar (cb)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement FolderBrowserScreen with browseSet endpoint and breadcrumb ↵Paul Buetow
navigation (va) Adds a FolderBrowserScreen that calls browseSet on init and pull-to-refresh, renders subfolders first (with cover via setFolderCoverUrl) then media items, and provides a scrollable breadcrumb bar for navigating the folder hierarchy. Key changes: - player-android/lib/screens/folder_browser_screen.dart: new screen with loading/empty/error/refresh states, generation-counter cancellation, and no Dio import in the screen layer (DIP). - player-android/lib/api/player_api_client.dart: add setFolderCoverUrl() so screens never access rawDio directly for URL construction (DIP). - player-android/lib/utils/duration_formatter.dart: extract shared formatDuration() from MediaGridScreen to eliminate the DRY violation. - player-android/lib/screens/media_grid_screen.dart: delegate to formatDuration() from the shared utility. - player-android/lib/utils/error_mappers.dart: add folderErrorMessage(). - player-android/lib/app_routes.dart: add folderBrowser and folderBrowserPath(). - player-android/lib/router.dart: wire /browse/:setId GoRoute. - player-android/test/screens/folder_browser_screen_test.dart: 16 widget tests covering renders, breadcrumbs, folder/media tap navigation, empty, error, retry, and pull-to-refresh. All 298 tests pass; flutter analyze reports no issues. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement MySharesScreen with revoke/copy-link actions reachable from ↵Paul Buetow
Settings (8b) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement all remaining PlayerApiClient stubs and add unit testsPaul Buetow
Implements every previously-unimplemented method in DioPlayerApiClient: shares (listSharesForMedia, listMyShares, revokeShare), public shared-media endpoints (getSharedMediaPage, streamSharedMedia, getSharedThumbnail, downloadSharedMedia), config (getConfig), sets (getSetCover, updateSetCover, uploadToSet), media helpers (regenerateThumbnail, deleteMedia, restoreMedia), progress batch (batchUpdateProgress), podcasts (listPodcasts, listEpisodes, downloadEpisode, toggleEpisodeComplete), admin users/permissions/scanner (listUsers, createUser, deleteUser, listPermissions, grantPermission, revokePermission, triggerRescan, getScanProgress, listTrash), and API tokens (listAPITokens, createAPIToken, revokeAPIToken). Adds listAPITokens/createAPIToken/revokeAPIToken/batchUpdateProgress to the abstract PlayerApiClient. Corrects listPermissions return type from List<Map> to Map<String,dynamic> to match the server's single-object response. Adds 18 new unit tests covering shares (listMyShares success/empty/401, revokeShare success/404/401) and podcasts (listEpisodes success/pagination/ empty/401, toggleEpisodeComplete success/404/401). All 264 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement NotesEditorScreen with auto-save debounce and clear confirmation (6b)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement Tag picker with optimistic UI, autocomplete, and delete chips on ↵Paul Buetow
MediaDetailScreen (5b) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Wrap AudioPlayerScreen in audio_service for background playback (1b)Paul Buetow
Creates PlayerAudioHandler (BaseAudioHandler + SeekHandler) that wraps just_audio's AudioPlayer and bridges it to the Android media session: background foreground-service playback, lock-screen / notification controls (play/pause/seek/skip ±15 s), audio focus, and Bluetooth headset events all handled by audio_service. Key design decisions: - Handler registered once via AudioService.init in main() and injected into ProviderScope via overrideWithValue (DIP: no global mutable var). - Progress-sync timer stays in the screen so PlayerApiClient is never imported by the handler (SRP boundary preserved). - Seek bar onChanged routes through handler.seek() so the notification position updates on slider drags (Law of Demeter fix). - _initPlayer refactored into _buildAuthHeaders / _loadSource / _resumeFromSavedPosition helpers (each ≤30 lines, SoC). - Tests override audioHandlerProvider with _FakePlayerAudioHandler to avoid platform-channel calls; all 221 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement ContinueWatchingScreen with resume cards and progress routing (2b)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement favorites toggle with optimistic UI in MediaDetailScreen and ↵Paul Buetow
MediaGridScreen (4b) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement SearchFilterBar with debounce, type/favorites/sort filters for ↵Paul Buetow
MediaGridScreen (wa) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement PodcastListScreen and SubscribeDialog with podcast feed management ↵Paul Buetow
(ab) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement CreateShareDialog with expiry/max-uses, shareUrl abstraction (7b)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement AudioPlayerScreen with just_audio, progress sync, and bearer auth (0b)Paul Buetow
Replaces the placeholder AudioPlayerScreen with a full implementation: - Streams audio via just_audio AudioPlayer with Bearer token in headers - Progress sync mirrors VideoPlayerScreen exactly: 5 s timer, isPlaying guard, 95 % finished threshold, _finishedEmitted guard, same dispose order - UI: cover art placeholder, StreamBuilder-backed seek bar, play/pause, skip ±15 s (fast_rewind/fast_forward icons), speed selector (0.5–2x) - Resume from server-saved position via getMediaProgress on init - Error state with retry button; all async continuations guard on mounted - Widget tests cover: loading spinner, error view structure, AppBar title, URL resolution; audio_session channel mocked to unblock headless tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement VideoPlayerScreen with chewie, progress sync, and bearer auth (za)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement MediaDetailScreen with metadata, favorite toggle, player routing (ya)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Add video/audio player packages, routes, and placeholder screens (task xa)Paul Buetow
- pubspec.yaml: add video_player ^2.9.2, chewie ^1.8.5, just_audio ^0.9.42, audio_service ^0.18.15 with descriptive comments explaining each package's role. - AndroidManifest.xml: add FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, and WAKE_LOCK permissions; declare AudioService with foregroundServiceType= mediaPlayback and MediaButtonReceiver for hardware media button support. INTERNET was already present — not duplicated. - app_routes.dart: add videoPlayer (/video/:mediaId) and audioPlayer (/audio/:mediaId) route constants plus videoPlayerPath/audioPlayerPath helpers. - router.dart: wire GoRoutes for the two new paths, forwarding mediaId from path params and optional mediaUrl from route extra. - screens/video_player_screen.dart: placeholder VideoPlayerScreen accepting mediaId + mediaUrl, importing chewie and video_player for resolution check. - screens/audio_player_screen.dart: placeholder AudioPlayerScreen accepting mediaId + mediaUrl, importing just_audio and audio_service for resolution check. flutter pub get and flutter analyze both pass clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement MediaGridScreen with grid, loading/empty/error states, and widget ↵Paul Buetow
tests (ua) - Replace placeholder MediaGridScreen with a full implementation: loads media via listMedia(setId:), renders a Material 3 2-column grid of thumbnail cards with filename, type icon, and duration overlay. - Add thumbnailUrl(int mediaId) to PlayerApiClient so screen files construct thumbnail URLs without importing Dio (DIP). - Add mediaErrorMessage() top-level helper to error_mappers.dart (Open-Closed Principle; matches setsErrorMessage pattern). - Update router.dart to forward the set name as a route extra so MediaGridScreen shows the name in the app bar without extra API calls. - Update home_screen.dart _SetCard tap to pass set name as route extra. - Add 10 widget tests covering: loading, grid render, tap navigation to /media/:id, empty state, error state + retry, and pull-to-refresh. - All 109 tests pass; flutter analyze reports no issues. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement SetsListScreen with Material 3 grid, pull-to-refresh, and widget ↵Paul Buetow
tests (ta) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement SettingsScreen with AuthGuard and settings persistence (sa)Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Add GET /api/v1/auth/count endpoint and first-run routing in Android appPaul Buetow
Server: expose a public countUsers endpoint (GET /api/v1/auth/count) so mobile clients can detect first-run (count=0) without a session. Android: wire countUsers via DioPlayerApiClient, add firstRunProvider (FutureProvider), update go_router redirect to drive /bootstrap vs /login based on the count, rework LoginScreen to handle loading/error states, and add widget tests for the new login screen and smoke-test updates. Fix review issues: correct FutureProvider cache-lifetime comment in first_run_provider.dart; add TestServer_CountUsers covering zero-users and users-exist cases to handlers_test.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21Implement BootstrapScreen with widget tests (qa)Paul Buetow
Adds the first-run admin-account setup screen (POST /api/v1/auth/bootstrap), wires it into the go_router redirect guard, and extracts the pure _dioErrorMessage helper to a top-level function to satisfy SRP. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20Implement DioPlayerApiClient with real HTTP bodies for core API methods (oa)Paul Buetow
Replace UnimplementedError stubs with concrete Dio calls for bootstrap, login, logout, listSets, browseSet, listMedia, getMedia, streamMedia, downloadMedia, getThumbnail, healthz, and readyz. Wire DioPlayerApiClient into the Riverpod provider. Add http_mock_adapter dev dependency and 25 unit tests covering success + error paths for all implemented methods. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20Add go_router + Riverpod wiring and extract SOLID-clean screen/nav layers (na)Paul Buetow
Introduces flutter_riverpod and go_router dependencies. Wires up the app with a Riverpod-managed GoRouter, auth-guarded redirect logic, and a shared navigator key used by DioClient for 401 → /login redirects. SOLID fixes applied: - navigatorKey extracted to navigation_key.dart (DIP: breaks cross-layer import from router.dart into api_client_provider.dart) - AppRoutes extracted to app_routes.dart (SRP + avoids circular import when screen files reference route constants) - LoginScreen, HomeScreen, MediaDetailScreen, ShareScreen each moved to their own file under lib/screens/ (SRP: router.dart is routing-only) - router.dart re-exports AppRoutes for backward-compatible callers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20Add DioClient with auth interceptors and refactor PlayerApiClient (ma)Paul Buetow
Introduces dio_client.dart with _AuthInterceptor (guards Bearer token injection with containsKey so callers can override Authorization) and _UnauthorizedInterceptor (private fields, 401 → login redirect). PlayerApiClient is refactored to accept a pre-configured Dio instance instead of raw credentials; adds dio + flutter_secure_storage deps. Fixes review issues: Auth header comment/behavior corrected, private fields on _UnauthorizedInterceptor, unused import removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20Fix PlayerApiClient const constructor and Media.tags deserialization (d9+e9)Paul Buetow
- Remove const from PlayerApiClient constructor (Uri is not const-constructable) - Replace .cast<String>() with .whereType<String>().toList() in Media.fromJson to silently drop non-string and null tag elements instead of throwing TypeError - Add regression test: tags [1, 'valid', null] deserialized as ['valid'] Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20Guard dateTimeFromJson against malformed date strings (c9)Paul Buetow
Wrap DateTime.parse in try/catch so a malformed or non-ISO-8601 date string from the server degrades to null instead of throwing a FormatException that crashes model deserialization across Media, MediaSet, User, PodcastFeed, PodcastEpisode, PlaybackHint, Share, and Note. Add player-android/test/json_helpers_test.dart with 11 tests covering null, empty, non-string, valid ISO, and malformed inputs (regression guard for the original crash). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18Add named routes and widget smoke tests to player-androidPaul Buetow
Adds HomeScreen ('/') and NowPlayingScreen ('/now-playing') as the first two named routes in PlayerAndroidApp, replacing the anonymous home widget. Widget smoke tests verify that the app starts on the library screen, navigates to the now-playing screen on button tap, and back-navigates correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18Add Android testing strategy proposalPaul Buetow
Produces player-android/docs/testing-strategy.md as a decision gate covering test scope, CI emulator options, integration_test vs Patrol, and LLM access mode. Reviewed by sub-agent; all issues addressed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>