fix(headers): fit artwork and unify toolbar controls

This commit is contained in:
zarzet committed 2026-10-15 02:52:23 +07:00
1 parent 1bae2e6b16
commit d28e3f65b1
7 files changed
+82 -90

No files matched your search

+6 -10
View File
@@ -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),
),
),
),
+21 -52
View File
@@ -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),
),
);
}
+8 -3
View File
@@ -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,
+11 -19
View File
@@ -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<TrackMetadataScreen>
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),
),
],
+30 -4
View File
@@ -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
+3 -1
View File
@@ -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);
+3 -1
View File
@@ -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<void> 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);