Pārlūkot izejas kodu

feat: show Jellyfin album year in album header

Adds the Jellyfin ProductionYear in parentheses after the album name on the album page, e.g. Ascension (2025), without duplicating a year already in the name. No version bump or build; staged for the next release.
Krasimir Kazakov 5 dienas atpakaļ
vecāks
revīzija
5cfc576893

+ 1 - 1
AGENTS.md

@@ -116,7 +116,7 @@ notifyListeners();
 - Connection attempts are bounded (three failures, 12 s connect watchdog). After they are exhausted the card shows **Retry connection**; do not restore an infinite reconnect loop.
 - When Bluetooth is off, the OBD-II card shows **Open Bluetooth settings**. On resume the controller checks adapter power state and reuses the cached bonded list; it only lists devices when the list is unknown, after **Find**, or when the card opens.
 - Pairing happens in Android settings, never in CarDash.
-- On Home, the circular ring is song progress; its centre is play/pause and the Mute pill is separate. Jellyfin playback title/progress comes from `JellyfinPlaybackController`, so it works without notification access; other apps still need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing. The album page no longer uses `VolumeCard`: it has one seekable progress slider with elapsed/total time, a centered previous / play-pause / next row, and a compact volume card (−, Mute/Unmute, +) beside the now-playing block. The Home card keeps the ring. The progress/transport card is bottom-aligned with the album cover in both the wide and narrow layouts; the transport buttons are 80/62 px and the compact volume steps are 40 px. The album slider seeks on release through `JellyfinPlaybackController.seek`; a local drag value keeps the thumb stable until the next poll.
+- On Home, the circular ring is song progress; its centre is play/pause and the Mute pill is separate. Jellyfin playback title/progress comes from `JellyfinPlaybackController`, so it works without notification access; other apps still need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing. The album page no longer uses `VolumeCard`: it has one seekable progress slider with elapsed/total time, a centered previous / play-pause / next row, and a compact volume card (−, Mute/Unmute, +) beside the now-playing block. The Home card keeps the ring. The progress/transport card is bottom-aligned with the album cover in both the wide and narrow layouts; the transport buttons are 80/62 px and the compact volume steps are 40 px. The album header appends the Jellyfin `ProductionYear` in parentheses to the name when present, e.g. `Ascension (2025)`, and does not duplicate a year already in the name. The album slider seeks on release through `JellyfinPlaybackController.seek`; a local drag value keeps the thumb stable until the next poll.
 - Reinstalling the app can clear its HOME role; re-run `tool/install.sh` or use Settings > Home app.
 - CarDash can also run as a normal app without the HOME role. The launcher activity intentionally has no `excludeFromRecents`, so it appears in recents and can be switched back to. `resumeWhilePausing` and `alwaysRetainTaskState` keep the Home transition and task state sane. Android may still kill any background app: there is deliberately no keep-alive foreground service, `persistent` flag or battery-exemption request. On vendor firmware, tell the user to set CarDash to no battery restrictions / allow autostart. When CarDash holds the HOME role, Android may still filter the home task out of recents; use Home to return. Do not re-add `excludeFromRecents` without asking, and never add a launcher keep-alive FGS.
 

+ 3 - 1
README.md

@@ -335,7 +335,9 @@ the app resumes.
   play-pause / next buttons below it. The card is bottom-aligned with the album
   cover. A compact volume card (−, Mute / Unmute, +) sits to the right of the
   now-playing block. The heart adds or removes the album through the Jellyfin
-  API; the Favorites tab refreshes when you return.
+  API; the Favorites tab refreshes when you return. The album heading includes
+  the release year in parentheses, for example `Ascension (2025)`, when Jellyfin
+  provides it.
 - **Home tile**: if a Jellyfin album is active, the Home tile opens that album's
   page with the dashboard underneath, so Back returns to the Albums tab.
 - **Playback**: in-app Media3 with background playback, a media notification on

+ 4 - 0
docs/IMPLEMENTATION_PLAN.md

@@ -331,6 +331,10 @@ Android (Kotlin)
 
 ## 8. Change log
 
+- 2026-11-01 (unreleased): the Jellyfin album header shows the release year in
+  parentheses after the album name, e.g. `Ascension (2025)`, when Jellyfin
+  provides `ProductionYear`. It does not add a duplicate year already present in
+  the name. No version bump, APK or release has been made.
 - 2026-11-01: CarDash 1.2.5+25 aligns the Jellyfin album progress/transport card
   with the bottom of the album cover in both wide and narrow layouts. The
   previous/play-pause/next buttons grow to 62/80 px and the compact volume steps

+ 5 - 1
lib/features/jellyfin/jellyfin_album_screen.dart

@@ -527,6 +527,10 @@ class _AlbumHeader extends StatelessWidget {
   Widget build(BuildContext context) {
     final colors = context.colors;
     final theme = Theme.of(context);
+    final year = album.year;
+    final title = year == null || year <= 0 || album.name.contains('($year)')
+        ? album.name
+        : '${album.name} ($year)';
     return Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
@@ -544,7 +548,7 @@ class _AlbumHeader extends StatelessWidget {
               ),
               const SizedBox(height: 2),
               Text(
-                album.name,
+                title,
                 maxLines: 2,
                 overflow: TextOverflow.ellipsis,
                 style: theme.textTheme.headlineLarge,

+ 4 - 3
test/jellyfin_album_screen_test.dart

@@ -38,7 +38,8 @@ void main() {
       jellyfin,
       pollInterval: const Duration(hours: 1),
     );
-    await playback.playAlbum(album('album-1'));
+    final albumOne = album('album-1', year: 2025);
+    await playback.playAlbum(albumOne);
 
     final system = SystemController(FakeSystemPlatform());
 
@@ -54,13 +55,13 @@ void main() {
         ],
         child: MaterialApp(
           theme: AppTheme.light(),
-          home: JellyfinAlbumScreen(album: album('album-1')),
+          home: JellyfinAlbumScreen(album: albumOne),
         ),
       ),
     );
     await tester.pumpAndSettle();
 
-    expect(find.text('Album album-1'), findsOneWidget);
+    expect(find.text('Album album-1 (2025)'), findsOneWidget);
     expect(find.text('Tracks'), findsOneWidget);
     expect(find.text('Track t1'), findsWidgets);
     expect(find.text('Track t2'), findsOneWidget);

+ 2 - 1
test/jellyfin_fakes.dart

@@ -24,12 +24,13 @@ final testJellyfinConnection = JellyfinConnection(
   userName: 'wasp',
 );
 
-JellyfinAlbum album(String id, {String? imageTag}) {
+JellyfinAlbum album(String id, {String? imageTag, int? year}) {
   return JellyfinAlbum(
     id: id,
     name: 'Album $id',
     artist: 'Artist $id',
     imageTag: imageTag,
+    year: year,
   );
 }