From d28e3f65b1fc121cf0f66596cbc5c7ceda3056ce Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:52:23 +0700 Subject: [PATCH] fix(headers): fit artwork and unify toolbar controls --- lib/screens/home_tab_widgets.dart | 16 ++-- lib/screens/library_tracks_folder_screen.dart | 73 ++++++------------- lib/screens/track_metadata_cards.dart | 11 ++- lib/screens/track_metadata_screen.dart | 30 +++----- lib/widgets/album_detail_header.dart | 34 ++++++++- test/track_metadata_cover_search_test.dart | 4 +- test/track_metadata_replaygain_test.dart | 4 +- 7 files changed, 82 insertions(+), 90 deletions(-) diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index db0e2f1e..215ee47e 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -988,17 +988,13 @@ class _ArtistLoadingScaffold extends StatelessWidget { leadingWidth: kToolbarHeight + edgeInset, leading: Padding( padding: EdgeInsets.only(left: edgeInset), - child: IconButton( - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon(Icons.arrow_back, color: Colors.white), + // Centered so the tight leading slot cannot stretch it. + child: Center( + child: HeaderCircleButton( + icon: Icons.arrow_back, + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () => Navigator.pop(context), ), - onPressed: () => Navigator.pop(context), ), ), ), diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index 0afad7c4..8c5aa496 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -635,75 +635,44 @@ class _LibraryTracksFolderScreenState : null, appBarActions: [ if (!isSelectionMode && entries.isNotEmpty) - IconButton( + HeaderCircleButton( + icon: Icons.ios_share, + iconSize: 20, tooltip: context.l10n.collectionExportM3u, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon(Icons.ios_share, color: Colors.white, size: 20), - ), onPressed: () => _exportAsM3u8(context, title, entries), ), if (isPlaylistMode && !isSelectionMode) ...[ - IconButton( + const SizedBox(width: 8), + HeaderCircleButton( + icon: Icons.edit_outlined, + iconSize: 20, tooltip: context.l10n.collectionRenamePlaylist, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.edit_outlined, - color: Colors.white, - size: 20, - ), - ), onPressed: () { final id = widget.playlistId ?? playlist?.id; if (id == null || id.isEmpty) return; _showRenamePlaylistDialog(context, id, playlist?.name ?? title); }, ), - IconButton( + const SizedBox(width: 8), + HeaderCircleButton( + icon: Icons.camera_alt_outlined, + iconSize: 20, tooltip: context.l10n.collectionPlaylistChangeCover, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.camera_alt_outlined, - color: Colors.white, - size: 20, - ), - ), onPressed: () => _showCoverOptionsSheet(context, hasCustomCover), ), ], ], - leading: IconButton( - tooltip: isSelectionMode - ? MaterialLocalizations.of(context).closeButtonTooltip - : MaterialLocalizations.of(context).backButtonTooltip, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: Icon( - isSelectionMode ? Icons.close : Icons.arrow_back, - color: Colors.white, - ), + // Centered so the tight leading slot cannot stretch it. + leading: Center( + child: HeaderCircleButton( + icon: isSelectionMode ? Icons.close : Icons.arrow_back, + tooltip: isSelectionMode + ? MaterialLocalizations.of(context).closeButtonTooltip + : MaterialLocalizations.of(context).backButtonTooltip, + onPressed: isSelectionMode + ? exitSelectionMode + : () => Navigator.pop(context), ), - onPressed: isSelectionMode - ? exitSelectionMode - : () => Navigator.pop(context), ), ); } diff --git a/lib/screens/track_metadata_cards.dart b/lib/screens/track_metadata_cards.dart index d2f20de1..a465fffa 100644 --- a/lib/screens/track_metadata_cards.dart +++ b/lib/screens/track_metadata_cards.dart @@ -188,9 +188,14 @@ extension _TrackMetadataCards on _TrackMetadataScreenState { // Centered square cover over a blurred backdrop (same layout as the album // header) so the Hero flight from a list thumbnail keeps its square shape. - final coverSize = (MediaQuery.sizeOf(context).width * 0.5) - .clamp(150.0, 210.0) - .toDouble(); + // Landscape headers are short: the cover also yields to the height left + // below the toolbar after the title block, so nothing rises into it. + final toolbarBottom = MediaQuery.paddingOf(context).top + kToolbarHeight; + const textBlockHeight = 190.0; + final coverSize = math.min( + (MediaQuery.sizeOf(context).width * 0.5).clamp(150.0, 210.0), + math.max(96.0, expandedHeight - toolbarBottom - textBlockHeight), + ); return Stack( fit: StackFit.expand, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 47afbb3a..145dc92c 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math; import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; import 'package:spotiflac_android/theme/app_motion.dart'; @@ -745,31 +746,22 @@ class _TrackMetadataScreenState extends ConsumerState leadingWidth: kToolbarHeight + edgeInset, leading: Padding( padding: EdgeInsets.only(left: edgeInset), - child: IconButton( - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon(Icons.arrow_back, color: Colors.white), + // Centered so the tight leading slot cannot stretch it. + child: Center( + child: HeaderCircleButton( + icon: Icons.arrow_back, + tooltip: MaterialLocalizations.of( + context, + ).backButtonTooltip, + onPressed: _popWithMetadataResult, ), - onPressed: _popWithMetadataResult, ), ), actionsPadding: EdgeInsets.only(right: edgeInset), actions: [ - IconButton( + HeaderCircleButton( + icon: Icons.more_vert, tooltip: MaterialLocalizations.of(context).showMenuTooltip, - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.4), - shape: BoxShape.circle, - ), - child: const Icon(Icons.more_vert, color: Colors.white), - ), onPressed: () => _showOptionsMenu(context, ref, colorScheme), ), ], diff --git a/lib/widgets/album_detail_header.dart b/lib/widgets/album_detail_header.dart index 59e07c2b..e108030d 100644 --- a/lib/widgets/album_detail_header.dart +++ b/lib/widgets/album_detail_header.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/cupertino.dart'; @@ -128,7 +129,13 @@ class AlbumDetailHeader extends StatelessWidget { } final scheme = Theme.of(context).colorScheme; final edgeInset = detailHeaderEdgeInset(context); - final artworkSize = MediaQuery.sizeOf(context).width.clamp(0.0, 440.0); + final screenSize = MediaQuery.sizeOf(context); + // Landscape tablets are short: the artwork also yields to the height so + // the title and actions stay on the first screen. + final artworkSize = math.min( + screenSize.width.clamp(0.0, 440.0), + math.max(240.0, screenSize.height * 0.55), + ); final details = _buildMornyeDetails(context); Widget toolbar({bool collapsed = true}) => SliverAppBar( pinned: true, @@ -201,6 +208,8 @@ class AlbumDetailHeader extends StatelessWidget { ), SliverToBoxAdapter( child: Stack( + // Centers the metadata when it is narrower than the screen. + alignment: Alignment.topCenter, children: [ Positioned.fill( child: squareArtwork @@ -349,9 +358,26 @@ class AlbumDetailHeader extends StatelessWidget { if (coverBuilder != null) ...[ Builder( builder: (context) { - final coverSize = (constraints.maxWidth * 0.5) - .clamp(150.0, 210.0) - .toDouble(); + // Landscape headers are short: the cover also yields to + // the height left below the toolbar once the title, + // details and actions are placed, so it never rises + // into the status bar or toolbar buttons. + final toolbarBottom = + MediaQuery.paddingOf(context).top + kToolbarHeight; + final detailsHeight = + 40 + + 20 + + 58 + + (subtitle == null ? 0 : 28) + + (meta == null ? 0 : 34) + + (actions == null ? 0 : 64); + final coverSize = math.min( + (constraints.maxWidth * 0.5).clamp(150.0, 210.0), + math.max( + 96.0, + expandedHeight - toolbarBottom - detailsHeight, + ), + ); // Material uses the expressive extra-large corner. final coverRadius = context.isMornye ? tokens.radiusControl diff --git a/test/track_metadata_cover_search_test.dart b/test/track_metadata_cover_search_test.dart index 568e6165..3d755b0c 100644 --- a/test/track_metadata_cover_search_test.dart +++ b/test/track_metadata_cover_search_test.dart @@ -14,6 +14,8 @@ import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/widgets/app_choice_chip.dart'; +import 'support/app_icon_finder.dart'; + class _Extensions extends ExtensionNotifier { @override ExtensionState build() => const ExtensionState( @@ -163,7 +165,7 @@ void main() { ), ); await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.more_vert)); + await tester.tap(findAppIcon(Icons.more_vert)); await tester.pumpAndSettle(); expect(find.text('Edit audio file'), findsNothing); expect(find.text('Similar songs in Library'), findsNothing); diff --git a/test/track_metadata_replaygain_test.dart b/test/track_metadata_replaygain_test.dart index 1ee972de..98c828eb 100644 --- a/test/track_metadata_replaygain_test.dart +++ b/test/track_metadata_replaygain_test.dart @@ -10,6 +10,8 @@ import 'package:spotiflac_android/providers/download_history_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; +import 'support/app_icon_finder.dart'; + class _History extends DownloadHistoryNotifier { _History(this._item); final DownloadHistoryItem _item; @@ -140,7 +142,7 @@ void main() { expect(find.text(text), hasTags ? findsOneWidget : findsNothing); } Future openRemoval() async { - await tester.tap(find.byIcon(Icons.more_vert)); + await tester.tap(findAppIcon(Icons.more_vert)); await tester.pumpAndSettle(); expect(find.text('Edit audio file'), findsNothing); expect(find.text('Similar songs in Library'), findsNothing);