فهرست منبع

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 4 روز پیش
والد
کامیت
5cfc576893
6فایلهای تغییر یافته به همراه19 افزوده شده و 7 حذف شده
  1. 1 1
      AGENTS.md
  2. 3 1
      README.md
  3. 4 0
      docs/IMPLEMENTATION_PLAN.md
  4. 5 1
      lib/features/jellyfin/jellyfin_album_screen.dart
  5. 4 3
      test/jellyfin_album_screen_test.dart
  6. 2 1
      test/jellyfin_fakes.dart

+ 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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
   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
   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
   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
 - **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.
   page with the dashboard underneath, so Back returns to the Albums tab.
 - **Playback**: in-app Media3 with background playback, a media notification on
 - **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
 ## 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
 - 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
   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
   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) {
   Widget build(BuildContext context) {
     final colors = context.colors;
     final colors = context.colors;
     final theme = Theme.of(context);
     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(
     return Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
       children: [
@@ -544,7 +548,7 @@ class _AlbumHeader extends StatelessWidget {
               ),
               ),
               const SizedBox(height: 2),
               const SizedBox(height: 2),
               Text(
               Text(
-                album.name,
+                title,
                 maxLines: 2,
                 maxLines: 2,
                 overflow: TextOverflow.ellipsis,
                 overflow: TextOverflow.ellipsis,
                 style: theme.textTheme.headlineLarge,
                 style: theme.textTheme.headlineLarge,

+ 4 - 3
test/jellyfin_album_screen_test.dart

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

+ 2 - 1
test/jellyfin_fakes.dart

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