mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 04:42:02 +02:00
Add a selectable Mornye theme with shared glass controls, dialogs, sheets, notifications, and collection headers while retaining Material components. Update player artwork, lyrics transitions and highlighting, synchronized rolling time labels, volume controls, and compact navigation. Unify metadata, Library, search, and extension surfaces; preserve restored artwork and report extension load failures. Validation: full Flutter analyzer clean; 713 tests passed and 4 skipped; pinned Dart formatting and diff checks passed; signed ARM64 and ARM32 release APKs built and verified.
222 lines
8.4 KiB
Dart
222 lines
8.4 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
|
import 'package:spotiflac_android/widgets/app_alert_dialog.dart';
|
|
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
|
|
|
void main() {
|
|
for (final brightness in Brightness.values) {
|
|
for (final reduceMotion in [false, true]) {
|
|
testWidgets(
|
|
'confirmation keeps blur during entry and cancels promptly ($brightness, reduce motion: $reduceMotion)',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
child: MaterialApp(
|
|
theme: MornyeTheme.build(brightness),
|
|
builder: (context, child) => MediaQuery(
|
|
data: MediaQuery.of(
|
|
context,
|
|
).copyWith(disableAnimations: reduceMotion),
|
|
child: child!,
|
|
),
|
|
home: Builder(
|
|
builder: (context) => Scaffold(
|
|
body: TextButton(
|
|
child: const Text('Open'),
|
|
onPressed: () => showAppDialog<void>(
|
|
context: context,
|
|
builder: (context) => AppAlertDialog(
|
|
title: const Text('Delete Selected'),
|
|
content: const Text(
|
|
'This will also delete the files.',
|
|
),
|
|
actions: [
|
|
AppDialogAction(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Cancel'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('Open'));
|
|
await tester.pump();
|
|
for (final elapsed in [16, 64, 180]) {
|
|
await tester.pump(Duration(milliseconds: elapsed));
|
|
final blur = find.descendant(
|
|
of: find.byType(AppAlertDialog),
|
|
matching: find.byType(BackdropFilter),
|
|
);
|
|
expect(blur, findsOneWidget);
|
|
// An opacity layer isolates the filter from the underlying page
|
|
// until the route finishes entering.
|
|
final fades = tester.widgetList<FadeTransition>(
|
|
find.ancestor(of: blur, matching: find.byType(FadeTransition)),
|
|
);
|
|
expect(fades.every((fade) => fade.opacity.value == 1), isTrue);
|
|
final bodyContext = tester.element(
|
|
find.text('This will also delete the files.'),
|
|
);
|
|
expect(
|
|
DefaultTextStyle.of(bodyContext).style.color,
|
|
Theme.of(bodyContext).colorScheme.onSurface,
|
|
);
|
|
}
|
|
final panel = find.byType(MornyeGlassPanel);
|
|
final openWidth = tester.getRect(panel).width;
|
|
await tester.tap(find.text('Cancel'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
if (!reduceMotion) {
|
|
// Closing must move on the first frame, rather than holding the
|
|
// fully open scale while a reversed entrance curve settles.
|
|
expect(tester.getRect(panel).width, lessThan(openWidth - 1));
|
|
await tester.pump(const Duration(milliseconds: 164));
|
|
await tester.pump();
|
|
}
|
|
expect(find.byType(AppAlertDialog), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
for (final mornye in [true, false]) {
|
|
testWidgets(
|
|
'confirmation preserves cancel and confirm results (Mornye: $mornye)',
|
|
(tester) async {
|
|
bool? result;
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
child: MaterialApp(
|
|
theme: mornye ? MornyeTheme.build(Brightness.dark) : ThemeData(),
|
|
home: Builder(
|
|
builder: (context) => Scaffold(
|
|
body: TextButton(
|
|
child: const Text('Open'),
|
|
onPressed: () async {
|
|
result = await showAppDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AppAlertDialog(
|
|
title: const Text('Remove extension'),
|
|
content: const Text('Remove this example extension?'),
|
|
actions: [
|
|
AppDialogAction(
|
|
isDefault: true,
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Cancel'),
|
|
),
|
|
AppDialogAction(
|
|
filled: true,
|
|
isDestructive: true,
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Remove'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Open'));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.byType(MornyeGlassPanel),
|
|
mornye ? findsOneWidget : findsNothing,
|
|
);
|
|
expect(
|
|
find.byType(AlertDialog),
|
|
mornye ? findsNothing : findsOneWidget,
|
|
);
|
|
if (mornye) {
|
|
final remove = find.widgetWithText(CupertinoButton, 'Remove');
|
|
final cancel = find.widgetWithText(CupertinoButton, 'Cancel');
|
|
expect(
|
|
tester.getRect(remove).bottom,
|
|
lessThan(tester.getRect(cancel).top),
|
|
);
|
|
expect(tester.getRect(remove).width, tester.getRect(cancel).width);
|
|
expect(
|
|
DefaultTextStyle.of(
|
|
tester.element(find.text('Remove')),
|
|
).style.color,
|
|
MornyeTheme.build(Brightness.dark).colorScheme.primary,
|
|
);
|
|
expect(find.byType(CupertinoAlertDialog), findsNothing);
|
|
expect(find.byType(FilledButton), findsNothing);
|
|
}
|
|
await tester.tap(find.text('Cancel'));
|
|
await tester.pumpAndSettle();
|
|
expect(result, isFalse);
|
|
|
|
await tester.tap(find.text('Open'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Remove'));
|
|
await tester.pumpAndSettle();
|
|
expect(result, isTrue);
|
|
|
|
await tester.tap(find.text('Open'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
expect(result, isNull);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|
|
|
|
testWidgets('glass confirmation keeps actions reachable with long text', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(393, 600);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
child: MaterialApp(
|
|
theme: MornyeTheme.build(Brightness.light),
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(textScaler: TextScaler.linear(1.5)),
|
|
child: AppAlertDialog(
|
|
title: const Text('Delete these tracks from your library?'),
|
|
content: Text(
|
|
List.filled(12, 'This removes the selected files.').join(' '),
|
|
),
|
|
actions: [
|
|
AppDialogAction(onPressed: () {}, child: const Text('Cancel')),
|
|
AppDialogAction(
|
|
isDestructive: true,
|
|
onPressed: () {},
|
|
child: const Text('Delete from Library'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Cancel').hitTestable(), findsOneWidget);
|
|
expect(find.text('Delete from Library').hitTestable(), findsOneWidget);
|
|
final scroll = find.byType(SingleChildScrollView).first;
|
|
await tester.drag(scroll, const Offset(0, -150));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Cancel').hitTestable(), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|