Skip to content

Full-width background image in app bar headers#157

Merged
phanan merged 6 commits into
masterfrom
feat/app-bar-full-width-image
Mar 29, 2026
Merged

Full-width background image in app bar headers#157
phanan merged 6 commits into
masterfrom
feat/app-bar-full-width-image

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented Mar 29, 2026

Summary

  • Remove the centered cover image from app bar headers
  • Background image now fills full width/height with no blur, fading to transparent at the bottom
  • Remove unused CoverImageStack and CoverImage classes
  • Clean up dead cover image variables from all detail screens (album, artist, playlist, podcast, songs, queue, favorites, recently played, downloaded, genre)
  • Net deletion of ~290 lines

Test plan

  • Open album details — verify full-width album art with bottom fade, no centered thumbnail
  • Open artist details — same behavior with artist image
  • Open playlist details (with and without cover) — verify correct rendering
  • Open podcast details — verify full-width podcast art
  • Open screens without background images (songs, queue, favorites, etc.) — verify no regression
  • Scroll to collapse the header — verify title and collapsed bar still work
  • Run flutter test

Summary by CodeRabbit

  • Style

    • Simplified app/header visuals across many screens: removed layered blurred overlays and separate cover tiles, now using a single background image with adjusted masking/gradient and a consistent default when none provided.
  • Chores

    • Removed the standalone Queue screen and its route/export.
    • Swapped the order of "Albums" and "Genres" in the Library navigation.
  • Tests

    • Updated UI tests to match the simplified header/background behavior.

Remove the centered cover image from the app bar header. The background
image now fills the full width and height with no blur, fading to
transparent at the bottom via ShaderMask. Remove unused CoverImageStack
and CoverImage classes and clean up dead cover image code from all
detail screens.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 613ef4d4-35c2-4ed4-b2a7-4d712fe322d0

📥 Commits

Reviewing files that changed from the base of the PR and between 0e77224 and a7eca0b.

📒 Files selected for processing (3)
  • lib/ui/screens/album_details.dart
  • lib/ui/screens/artist_details.dart
  • lib/ui/screens/podcast_details.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/ui/screens/album_details.dart
  • lib/ui/screens/podcast_details.dart
  • lib/ui/screens/artist_details.dart

📝 Walkthrough

Walkthrough

Removed AppBar's coverImage API and the CoverImageStack/CoverImage widgets; simplified header/background rendering across multiple screens (replaced blurred/Heroed cover visuals with single background images). Deleted the Queue screen and its route/export. Swapped two Library menu items.

Changes

Cohort / File(s) Summary
AppBar widget & helpers
lib/ui/widgets/app_bar.dart
Removed coverImage parameter and CoverImageStack/CoverImage classes; added backgroundImageFromPlayables(...); replaced stacked flexible-space with a single ShaderMask (gradient middle stop -> 0.6); removed blur/cover rendering and related imports.
Screens — cover/background simplification
lib/ui/screens/album_details.dart, lib/ui/screens/artist_details.dart, lib/ui/screens/podcast_details.dart, lib/ui/screens/downloaded.dart, lib/ui/screens/favorites.dart, lib/ui/screens/genre_details.dart, lib/ui/screens/recently_played.dart, lib/ui/screens/songs.dart
Removed local _cover state, per-screen CoverImageStack initialization and separate cover Hero/blur widgets; AppBars now use backgroundImageFromPlayables(...) or a plain DecoratedBox background computed at build time.
Playlist details
lib/ui/screens/playlist_details.dart
Removed cached _cover and blurred ImageFiltered path; added _buildBackgroundImage(playlist, playables) to choose between DecoratedBox when playlist.hasCover or backgroundImageFromPlayables(playables) otherwise.
Queue screen removal & exports/routes
lib/ui/screens/queue.dart, lib/ui/screens/screens.dart, lib/router.dart
Deleted QueueScreen and _QueueScreenState; removed QueueScreen.routeName route mapping and removed export 'queue.dart'; from screens barrel.
Library menu change
lib/ui/screens/library.dart
Swapped first two navigation items: first is now Albums (CupertinoIcons.music_albums) → AlbumsScreen; second is now Genres (CupertinoIcons.guitars) → GenresScreen.
Tests
test/ui/widgets/app_bar_test.dart
Removed coverImage usage in tests; updated assertions to reflect new default background and shader behavior.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through pixels, soft and spry,

blurred frames smoothed out beneath the sky,
AppBar slimmed to single glowing hue,
Queue went quiet, menus shuffled too,
nibble a carrot — tidy code anew 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: replacing centered cover images with full-width background images in app bar headers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/app-bar-full-width-image

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Add backgroundImageFromPlayables helper that picks the first playable
with a custom image. Applied to: playlists without cover, favorites,
recently played, all songs, downloaded, queue, and genre details.
Playlists with an explicit cover still use that cover.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/ui/screens/artist_details.dart (1)

87-98: ⚠️ Potential issue | 🟡 Minor

Add Hero widget wrapper to restore artist-details transition animation.

The code emits artist-hero-* tags from source widgets but this screen lacks a matching Hero destination, causing the navigation animation to regress. Wrap the background image in a Hero widget using the artist's ID:

Suggested fix
Hero(
  tag: 'artist-hero-${artist.id}',
  child: SizedBox.expand(
    child: DecoratedBox(
      decoration: BoxDecoration(
        image: DecorationImage(
          image: artist.image,
          fit: BoxFit.cover,
          alignment: Alignment.topCenter,
        ),
      ),
    ),
  ),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/artist_details.dart` around lines 87 - 98, Wrap the background
image DecoratedBox in a Hero widget so the destination matches the source hero
tags (use the tag 'artist-hero-${artist.id}'); locate the backgroundImage child
in the artist details screen (the SizedBox.expand/DecoratedBox block) and
replace that child with a Hero whose tag is built from artist.id and whose child
is the existing SizedBox.expand containing the DecoratedBox to restore the
transition animation.
lib/ui/screens/podcast_details.dart (1)

89-100: ⚠️ Potential issue | 🟡 Minor

Add missing Hero destination for podcast image transition.

The podcast card sources a Hero animation with tag podcast-hero-${podcast.id} (via AlbumArtistThumbnail with asHero: true), but the destination screen has no matching Hero wrapper around the image. Wrap the podcast image in podcast_details.dart line 89 with a Hero widget to complete the transition:

Hero(
  tag: 'podcast-hero-${podcast.id}',
  child: SizedBox.expand(
    child: DecoratedBox(
      decoration: BoxDecoration(
        image: DecorationImage(
          image: podcast.image,
          fit: BoxFit.cover,
          alignment: Alignment.topCenter,
        ),
      ),
    ),
  ),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/podcast_details.dart` around lines 89 - 100, The podcast image
background lacks a matching Hero destination for the source Hero tag used by
AlbumArtistThumbnail (asHero) — wrap the SizedBox.expand > DecoratedBox that
uses podcast.image with a Hero whose tag is 'podcast-hero-${podcast.id}' so the
transition completes; locate the backgroundImage builder that creates
SizedBox.expand/DecoratedBox and replace the child with a Hero(tag:
'podcast-hero-${podcast.id}', child: the existing SizedBox.expand child)
ensuring podcast.id and podcast.image are referenced in the tag and decoration
respectively.
lib/ui/screens/album_details.dart (1)

85-96: ⚠️ Potential issue | 🟡 Minor

Add Hero widget to match album thumbnail animation source.

Album thumbnails use Hero(tag: 'album-hero-${album.id}') for shared-axis transitions, but the destination header at lines 85–96 lacks a matching Hero wrapper. The background image should be wrapped in Hero(tag: 'album-hero-${album.id}') to preserve the animation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/album_details.dart` around lines 85 - 96, Wrap the header
background image widget in a Hero using the same tag as the thumbnail so the
shared-axis transition works: wrap the existing SizedBox.expand / DecoratedBox
block in Hero(tag: 'album-hero-${album.id}', child: ...). Locate the header
backgroundImage construction in album_details.dart (the SizedBox.expand ->
DecoratedBox block) and replace it with a Hero whose child is the same
DecoratedBox; ensure the tag uses album.id exactly as the source thumbnails do.
🧹 Nitpick comments (1)
test/ui/widgets/app_bar_test.dart (1)

7-21: Consider adding a regression assert for “no foreground cover” in this test.

This case now validates only text, so it won’t catch accidental reintroduction of a centered cover element.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ui/widgets/app_bar_test.dart` around lines 7 - 21, The test 'renders
heading text' currently only asserts the heading string; add a regression assert
that no foreground cover is present by asserting the cover widget is absent when
rendering koel.AppBar with headingText — e.g. add an expectation that
find.byKey(ValueKey('foregroundCover')) (or the actual Key/Type used for the
centered cover in your AppBar implementation) yields findsNothing so accidental
reintroduction of the centered cover will fail the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lib/ui/screens/album_details.dart`:
- Around line 85-96: Wrap the header background image widget in a Hero using the
same tag as the thumbnail so the shared-axis transition works: wrap the existing
SizedBox.expand / DecoratedBox block in Hero(tag: 'album-hero-${album.id}',
child: ...). Locate the header backgroundImage construction in
album_details.dart (the SizedBox.expand -> DecoratedBox block) and replace it
with a Hero whose child is the same DecoratedBox; ensure the tag uses album.id
exactly as the source thumbnails do.

In `@lib/ui/screens/artist_details.dart`:
- Around line 87-98: Wrap the background image DecoratedBox in a Hero widget so
the destination matches the source hero tags (use the tag
'artist-hero-${artist.id}'); locate the backgroundImage child in the artist
details screen (the SizedBox.expand/DecoratedBox block) and replace that child
with a Hero whose tag is built from artist.id and whose child is the existing
SizedBox.expand containing the DecoratedBox to restore the transition animation.

In `@lib/ui/screens/podcast_details.dart`:
- Around line 89-100: The podcast image background lacks a matching Hero
destination for the source Hero tag used by AlbumArtistThumbnail (asHero) — wrap
the SizedBox.expand > DecoratedBox that uses podcast.image with a Hero whose tag
is 'podcast-hero-${podcast.id}' so the transition completes; locate the
backgroundImage builder that creates SizedBox.expand/DecoratedBox and replace
the child with a Hero(tag: 'podcast-hero-${podcast.id}', child: the existing
SizedBox.expand child) ensuring podcast.id and podcast.image are referenced in
the tag and decoration respectively.

---

Nitpick comments:
In `@test/ui/widgets/app_bar_test.dart`:
- Around line 7-21: The test 'renders heading text' currently only asserts the
heading string; add a regression assert that no foreground cover is present by
asserting the cover widget is absent when rendering koel.AppBar with headingText
— e.g. add an expectation that find.byKey(ValueKey('foregroundCover')) (or the
actual Key/Type used for the centered cover in your AppBar implementation)
yields findsNothing so accidental reintroduction of the centered cover will fail
the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f71103a-10de-4405-95ea-22e455c176f9

📥 Commits

Reviewing files that changed from the base of the PR and between eba04a3 and 3b62700.

📒 Files selected for processing (12)
  • lib/ui/screens/album_details.dart
  • lib/ui/screens/artist_details.dart
  • lib/ui/screens/downloaded.dart
  • lib/ui/screens/favorites.dart
  • lib/ui/screens/genre_details.dart
  • lib/ui/screens/playlist_details.dart
  • lib/ui/screens/podcast_details.dart
  • lib/ui/screens/queue.dart
  • lib/ui/screens/recently_played.dart
  • lib/ui/screens/songs.dart
  • lib/ui/widgets/app_bar.dart
  • test/ui/widgets/app_bar_test.dart
💤 Files with no reviewable changes (6)
  • lib/ui/screens/queue.dart
  • lib/ui/screens/favorites.dart
  • lib/ui/screens/genre_details.dart
  • lib/ui/screens/songs.dart
  • lib/ui/screens/recently_played.dart
  • lib/ui/screens/downloaded.dart

phanan added 2 commits March 29, 2026 19:01
Apply a color matrix filter (1.2x contrast, -20 brightness) to header
background images, making edges sharper and low-resolution images look
crisper.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/ui/screens/downloaded.dart (1)

93-94: Consider caching the background image to avoid repeated computation on each build.

backgroundImageFromPlayables() iterates through the entire playables list and allocates new widget objects on every call. Since build() runs on each setState (search query changes, sort changes), this introduces O(n) scans and allocations per rebuild.

The previous implementation used a cached _cover state field with lazy initialization. Consider restoring a similar caching pattern:

♻️ Suggested caching approach
 class _DownloadedScreenState extends State<DownloadedScreen> {
   var _searchQuery = '';
+  Widget? _backgroundImage;

   `@override`
   Widget build(BuildContext context) {
     ...
             return CustomScrollView(
                 slivers: <Widget>[
                   AppBar(
                     headingText: 'Downloaded',
-                    backgroundImage: backgroundImageFromPlayables(
-                        provider.playables),
+                    backgroundImage: _backgroundImage ??=
+                        backgroundImageFromPlayables(provider.playables),

Note: You may need to invalidate _backgroundImage when the playables list changes if the provider can update independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/downloaded.dart` around lines 93 - 94, The build currently
calls backgroundImageFromPlayables(provider.playables) on every rebuild which
re-scans playables and allocates widgets; replace that with a cached state field
(e.g. _backgroundImage) and lazy-initialize it (compute once from
backgroundImageFromPlayables when null) and assign it in initState or the first
build; invalidate and recompute _backgroundImage whenever provider.playables
changes (use didUpdateWidget, a provider listener, or compare list
identity/length in setState) so updates still reflect new playables, and then
return the cached _backgroundImage from build instead of calling
backgroundImageFromPlayables each time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/ui/screens/downloaded.dart`:
- Around line 93-94: The build currently calls
backgroundImageFromPlayables(provider.playables) on every rebuild which re-scans
playables and allocates widgets; replace that with a cached state field (e.g.
_backgroundImage) and lazy-initialize it (compute once from
backgroundImageFromPlayables when null) and assign it in initState or the first
build; invalidate and recompute _backgroundImage whenever provider.playables
changes (use didUpdateWidget, a provider listener, or compare list
identity/length in setState) so updates still reflect new playables, and then
return the cached _backgroundImage from build instead of calling
backgroundImageFromPlayables each time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 651b6c47-1f7b-40e5-a8fe-845ced42ba0c

📥 Commits

Reviewing files that changed from the base of the PR and between 3b62700 and 066dad3.

📒 Files selected for processing (11)
  • lib/router.dart
  • lib/ui/screens/downloaded.dart
  • lib/ui/screens/favorites.dart
  • lib/ui/screens/genre_details.dart
  • lib/ui/screens/library.dart
  • lib/ui/screens/playlist_details.dart
  • lib/ui/screens/queue.dart
  • lib/ui/screens/recently_played.dart
  • lib/ui/screens/screens.dart
  • lib/ui/screens/songs.dart
  • lib/ui/widgets/app_bar.dart
💤 Files with no reviewable changes (3)
  • lib/ui/screens/screens.dart
  • lib/router.dart
  • lib/ui/screens/queue.dart
✅ Files skipped from review due to trivial changes (3)
  • lib/ui/screens/genre_details.dart
  • lib/ui/screens/songs.dart
  • lib/ui/screens/favorites.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/ui/screens/recently_played.dart
  • lib/ui/screens/playlist_details.dart
  • lib/ui/widgets/app_bar.dart

phanan added 2 commits March 29, 2026 19:11
Wrap background images in Hero widgets with matching tags so the
shared-axis transition from thumbnail to detail screen works.
@phanan phanan merged commit 64a293e into master Mar 29, 2026
2 checks passed
@phanan phanan deleted the feat/app-bar-full-width-image branch March 29, 2026 17:33
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.

1 participant