From c263feb7220cc7197761f453215925003a52a2d7 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 27 Sep 2026 01:13:02 +0700 Subject: [PATCH] fix(artist): restore artwork accents with neutral portrait fallback --- lib/screens/artist_screen_widgets.dart | 8 +-- lib/widgets/mornye_artist_header.dart | 7 ++- test/mornye_detail_headers_test.dart | 72 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/lib/screens/artist_screen_widgets.dart b/lib/screens/artist_screen_widgets.dart index e7d3b327..98a71cc4 100644 --- a/lib/screens/artist_screen_widgets.dart +++ b/lib/screens/artist_screen_widgets.dart @@ -88,7 +88,7 @@ extension _ArtistScreenSections on _ArtistScreenState { icon: CupertinoIcons.square_arrow_up, buttonSize: 50, iconSize: 24, - iconColor: Colors.white, + iconColor: colorScheme.primary, glassTintColor: Colors.white, glassTintOpacity: 0.10, tooltip: context.l10n.openInOtherServices, @@ -102,7 +102,7 @@ extension _ArtistScreenSections on _ArtistScreenState { CupertinoButton( padding: EdgeInsets.zero, borderRadius: BorderRadius.circular(37), - color: Colors.white, + color: colorScheme.primary, onPressed: () => _showDiscographyOptions(context, colorScheme, albums), child: SizedBox.square( @@ -112,7 +112,7 @@ extension _ArtistScreenSections on _ArtistScreenState { child: Icon( CupertinoIcons.arrow_down, size: 34, - color: colorScheme.surface, + color: colorScheme.onPrimary, ), ), ), @@ -123,7 +123,7 @@ extension _ArtistScreenSections on _ArtistScreenState { : CupertinoIcons.star, buttonSize: 50, iconSize: 24, - iconColor: Colors.white, + iconColor: colorScheme.primary, glassTintColor: Colors.white, glassTintOpacity: 0.10, tooltip: isFavoriteArtist diff --git a/lib/widgets/mornye_artist_header.dart b/lib/widgets/mornye_artist_header.dart index 754f627a..25f09301 100644 --- a/lib/widgets/mornye_artist_header.dart +++ b/lib/widgets/mornye_artist_header.dart @@ -90,16 +90,19 @@ class _MornyeArtistSurfaceState extends State { final logoColor = logo == null ? null : CoverPalette.sourceColor(logo, Brightness.dark); + // Keep the artwork accent for colorful portraits. A neutral + // portrait can acquire a blue Material accent; use white there, + // while allowing an explicit logo color to take precedence. final primary = widget.neutralActions ? Colors.white - : logoColor ?? palette.primary; + : logoColor ?? (useAccent ? palette.primary : Colors.white); final primaryLuminance = primary.computeLuminance(); final surfaceContrast = (primaryLuminance + 0.05) / (surface.computeLuminance() + 0.05); final onPrimary = widget.neutralActions ? Colors.black - : logoColor == null + : logoColor == null && useAccent ? palette.onPrimary : surfaceContrast >= 4.5 ? surface diff --git a/test/mornye_detail_headers_test.dart b/test/mornye_detail_headers_test.dart index e22c6689..1befc939 100644 --- a/test/mornye_detail_headers_test.dart +++ b/test/mornye_detail_headers_test.dart @@ -12,6 +12,78 @@ import 'package:spotiflac_android/widgets/collection_scaffold.dart'; import 'package:spotiflac_android/widgets/mornye_artist_header.dart'; void main() { + testWidgets('artist actions preserve artwork and logo accents', ( + tester, + ) async { + final directory = Directory.systemTemp.createTempSync('artist-palette-'); + addTearDown(() => directory.deleteSync(recursive: true)); + final neutralCover = File('${directory.path}/neutral.png'); + final coloredCover = File('${directory.path}/colored.png'); + final whiteLogo = File('${directory.path}/white-logo.png'); + final coloredLogo = File('${directory.path}/colored-logo.png'); + await tester.runAsync(() async { + for (final (file, color, isLogo) in [ + (neutralCover, const Color(0xff9da4a9), false), + (coloredCover, Colors.deepOrange, false), + (whiteLogo, Colors.white, true), + (coloredLogo, Colors.amber, true), + ]) { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawRect( + isLogo + ? const Rect.fromLTWH(20, 40, 60, 20) + : const Rect.fromLTWH(0, 0, 100, 100), + Paint()..color = color, + ); + final picture = recorder.endRecording(); + final image = await picture.toImage(100, 100); + final bytes = await image.toByteData(format: ui.ImageByteFormat.png); + image.dispose(); + picture.dispose(); + await file.writeAsBytes(bytes!.buffer.asUint8List()); + await CoverPalette.resolve(file.path, Brightness.dark); + } + }); + final artworkAccent = CoverPalette.peek( + coloredCover.path, + Brightness.dark, + )!.primary; + expect(artworkAccent, isNot(Colors.white)); + for (final brightness in Brightness.values) { + for (final (cover, logo, expected) in [ + (neutralCover, null, Colors.white), + (coloredCover, null, artworkAccent), + (coloredCover, whiteLogo, Colors.white), + (neutralCover, coloredLogo, Colors.amber), + ]) { + await tester.pumpWidget( + MaterialApp( + theme: MornyeTheme.build(brightness), + home: MornyeArtistSurface( + imageSource: cover.path, + logoSource: logo?.path, + child: const Text('Artist action'), + ), + ), + ); + await tester.pumpAndSettle(); + final scheme = Theme.of( + tester.element(find.text('Artist action')), + ).colorScheme; + expect(scheme.primary.toARGB32(), expected.toARGB32()); + final foreground = scheme.onPrimary.computeLuminance(); + final background = scheme.primary.computeLuminance(); + final contrast = foreground > background + ? (foreground + 0.05) / (background + 0.05) + : (background + 0.05) / (foreground + 0.05); + expect(contrast, greaterThanOrEqualTo(4.5)); + expect(tester.takeException(), isNull); + } + } + await tester.pumpWidget(const SizedBox()); + }); + testWidgets( 'local and online album actions share the neutral cover surface', (tester) async {