| Age | Commit message (Collapse) | Author |
|
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>
|
|
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>
|
|
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>
|
|
- 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>
|
|
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>
|
|
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>
|
|
- 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>
|
|
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>
|
|
- 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>
|
|
- 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>
|
|
- 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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
- _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>
|
|
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>
|
|
- 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>
|
|
- 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>
|
|
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>
|
|
- 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>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
Settings (8b)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
MediaDetailScreen (5b)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
MediaGridScreen (4b)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
MediaGridScreen (wa)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
(ab)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- 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>
|
|
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>
|
|
tests (ta)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
- 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>
|
|
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>
|
|
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>
|
|
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>
|