mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-05 07:36:53 +02:00
perf(mornye): adapt glass and player effects to device capabilities
This commit is contained in:
1 parent
b11e925ee4
commit
9ebf98ff6c
20 files changed
+500
-81
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user