perf(mornye): adapt glass and player effects to device capabilities

This commit is contained in:
zarzet committed 2026-09-30 23:42:00 +07:00
1 parent b11e925ee4
commit 9ebf98ff6c
20 files changed
+500 -81

No files matched your search

+37
View File
@@ -8,6 +8,8 @@ import 'package:analyzer/source/line_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart'
show LiquidGlassTabBar;
import 'package:spotiflac_android/l10n/app_localizations.dart';
import 'package:spotiflac_android/theme/app_theme.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
@@ -263,6 +265,41 @@ void main() {
);
}
testWidgets('theme selector skips the glass tab bar with reduced motion', (
tester,
) async {
var selected = 0;
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
home: MediaQuery(
data: const MediaQueryData(disableAnimations: true),
child: Scaffold(
body: StatefulBuilder(
builder: (context, setState) => MornyeSegmentedControl(
labels: const ['System', 'Light', 'Dark'],
selectedIndex: selected,
onChanged: (index) => setState(() => selected = index),
),
),
),
),
),
),
);
expect(find.byType(LiquidGlassTabBar), findsNothing);
expect(find.text('Dark'), findsOneWidget);
await tester.tap(find.text('Dark'));
await tester.pump();
expect(selected, 2);
expect(
tester.getCenter(find.text('System')).dx <
tester.getCenter(find.text('Dark')).dx,
isTrue,
);
expect(tester.takeException(), isNull);
});
testWidgets('glass search keeps text editing, submit and clear usable', (
tester,
) async {
+142
View File
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/app_switch.dart';
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
MornyeGlassLevel _levelFor({
required bool lowEnd,
required bool blur,
required bool liquidCapable,
}) {
final container = ProviderContainer(
overrides: [
lowEndDeviceProvider.overrideWithValue(lowEnd),
// Device default or the user's "always use blur effects" override.
backdropBlurEnabledProvider.overrideWithValue(blur),
deviceSupportsLiquidGlassProvider.overrideWithValue(liquidCapable),
],
);
addTearDown(container.dispose);
return container.read(mornyeGlassLevelProvider);
}
void main() {
test('runtime tiers map to Mornye glass levels', () {
// Low tier: arm32-only or low-RAM Android.
expect(
_levelFor(lowEnd: true, blur: false, liquidCapable: false),
MornyeGlassLevel.flat,
);
// Standard Android: frosted blur without shader passes.
expect(
_levelFor(lowEnd: false, blur: false, liquidCapable: false),
MornyeGlassLevel.frosted,
);
// iOS runs the standard tier but keeps the full material.
expect(
_levelFor(lowEnd: false, blur: false, liquidCapable: true),
MornyeGlassLevel.liquid,
);
// High-tier Android.
expect(
_levelFor(lowEnd: false, blur: true, liquidCapable: true),
MornyeGlassLevel.liquid,
);
// "Always use blur effects" restores the full material on any tier.
for (final lowEnd in [true, false]) {
expect(
_levelFor(lowEnd: lowEnd, blur: true, liquidCapable: false),
MornyeGlassLevel.liquid,
);
}
});
testWidgets('frosted level keeps blur but drops every shader surface', (
tester,
) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
deviceSupportsLiquidGlassProvider.overrideWithValue(false),
backdropBlurEnabledProvider.overrideWithValue(false),
],
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
home: Scaffold(
body: Column(
children: [
const MornyeGlass(
blurEnabled: true,
child: SizedBox(width: 200, height: 50),
),
MornyeSegmentedControl(
labels: const ['One', 'Two'],
selectedIndex: 0,
onChanged: (_) {},
),
AppSwitch(value: true, onChanged: (_) {}),
Consumer(
builder: (context, ref, _) => MornyeTabBar(
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.music_note),
label: 'Library',
),
],
selectedIndex: 0,
onSelected: (_) {},
blurEnabled: ref.watch(mornyeBlurEnabledProvider),
liquidGlass: ref.watch(mornyeLiquidGlassProvider),
),
),
],
),
),
),
),
);
await tester.pump();
expect(find.byType(LiquidGlassLens), findsNothing);
expect(find.byType(LiquidGlassTabBar), findsNothing);
expect(find.byType(LiquidGlassSwitch), findsNothing);
// The frosted material still samples the page behind it.
expect(find.byType(BackdropFilter), findsWidgets);
expect(find.text('Library'), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('liquid level keeps the shader glass', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
home: Scaffold(
body: Column(
children: [
const MornyeGlass(
blurEnabled: true,
child: SizedBox(width: 200, height: 50),
),
AppSwitch(value: true, onChanged: (_) {}),
],
),
),
),
),
);
await tester.pump();
expect(find.byType(LiquidGlassLens), findsWidgets);
expect(find.byType(LiquidGlassSwitch), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
+118
View File
@@ -1,14 +1,20 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/providers/player_artwork_video_provider.dart';
import 'package:spotiflac_android/providers/player_motion_artwork_provider.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/services/motion_artwork_store.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/mini_player.dart';
import 'package:spotiflac_android/widgets/overflow_marquee.dart';
import 'package:video_player/video_player.dart';
void main() {
const longTitle = 'A long song title with enough words to leave the viewport';
@@ -168,6 +174,66 @@ void main() {
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('bounded marquee rests at the start after its passes', (
tester,
) async {
Future<void> pumpBounded(String title, {int? maxCycles = 2}) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox(
width: 160,
child: OverflowMarquee(
resetKey: title,
maxCycles: maxCycles,
child: Text(
title,
maxLines: 1,
style: const TextStyle(fontSize: 16),
),
),
),
),
),
);
await tester.pump();
await tester.pump();
}
await pumpBounded(longTitle);
final scroll = controller(tester);
final distance = scroll.position.maxScrollExtent;
expect(distance, greaterThan(0));
// Two passes of pause + travel, then nothing else is scheduled.
var passes = 0;
var moving = false;
for (var i = 0; i < 600; i++) {
await tester.pump(const Duration(milliseconds: 250));
if (scroll.offset > 0) {
moving = true;
} else if (moving) {
moving = false;
passes++;
}
}
expect(passes, 2);
expect(controller(tester).offset, 0);
expect(tester.binding.transientCallbackCount, 0);
await tester.pump(const Duration(seconds: 10));
expect(controller(tester).offset, 0);
// Restoring full effects restarts a resting title without changing tracks.
await pumpBounded(longTitle, maxCycles: null);
await startScrolling(tester);
expect(controller(tester).offset, greaterThan(0));
// A new track starts a fresh count.
await pumpBounded('$longTitle again');
await startScrolling(tester);
expect(controller(tester).offset, greaterThan(0));
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('RTL titles scroll from the reading direction', (tester) async {
await pumpTitle(tester, direction: TextDirection.rtl);
expect(controller(tester).position.axisDirection, AxisDirection.left);
@@ -177,6 +243,58 @@ void main() {
await tester.pumpWidget(const SizedBox.shrink());
});
for (final liquid in [false, true]) {
testWidgets(
'Mornye mini player prepares the motion cover only with liquid glass '
'(liquid: $liquid)',
(tester) async {
final prepared = <String>[];
await tester.pumpWidget(
ProviderScope(
overrides: [
currentMediaItemProvider.overrideWith(
(ref) => Stream.value(
const MediaItem(
id: 'track',
title: 'Song',
album: 'Album',
artist: 'Artist',
),
),
),
playbackStateProvider.overrideWith((ref) => const Stream.empty()),
deviceSupportsLiquidGlassProvider.overrideWithValue(liquid),
backdropBlurEnabledProvider.overrideWithValue(false),
playerMotionArtworkProvider.overrideWith(
(ref, album) async =>
const MotionArtwork('file:///cover.mp4', aspectRatio: 1),
),
playerArtworkVideoProvider.overrideWith((ref, source) {
prepared.add(source);
return Completer<VideoPlayerController>().future;
}),
],
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Scaffold(
body: Center(child: SizedBox(width: 320, child: MiniPlayer())),
),
),
),
);
for (var i = 0; i < 4; i++) {
await tester.pump();
}
expect(find.text('Song'), findsWidgets);
expect(prepared, liquid ? ['file:///cover.mp4'] : isEmpty);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
},
);
}
for (final mornye in [false, true]) {
for (final compact in [false, true]) {
testWidgets(