mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
fix(ui): align local lyrics and refine Mornye selection
Read embedded lyrics through the playback metadata fallback when the lyrics reader misses them. Restore edge icon highlighting during navbar drags, tune selection contrast, and blend the profile name field into its card.
This commit is contained in:
1 parent
d8d46108bc
commit
1c535b493c
7 files changed
+217
-37
No files matched your search
@@ -45,6 +45,7 @@ void main() {
|
||||
Color? backdrop,
|
||||
Color? chromeSurface,
|
||||
Widget? body,
|
||||
ValueNotifier<int>? activeTab,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(393, 760);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -87,10 +88,10 @@ void main() {
|
||||
itemBuilder: (_, index) => Text('Row $index'),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: ValueListenableBuilder<bool>(
|
||||
valueListenable: chrome,
|
||||
builder: (_, collapsed, _) => MornyeBottomBar(
|
||||
collapsed: collapsed,
|
||||
bottomNavigationBar: ListenableBuilder(
|
||||
listenable: Listenable.merge([chrome, ?activeTab]),
|
||||
builder: (_, _) => MornyeBottomBar(
|
||||
collapsed: chrome.value,
|
||||
destinations: const [
|
||||
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
|
||||
NavigationDestination(
|
||||
@@ -106,8 +107,11 @@ void main() {
|
||||
label: 'Search',
|
||||
),
|
||||
],
|
||||
selectedIndex: 0,
|
||||
onSelected: (index) => selected = index,
|
||||
selectedIndex: activeTab?.value ?? 0,
|
||||
onSelected: (index) {
|
||||
selected = index;
|
||||
activeTab?.value = index;
|
||||
},
|
||||
onHome: chrome.expand,
|
||||
onSearch: () => searches++,
|
||||
blurEnabled: blur,
|
||||
@@ -123,6 +127,73 @@ void main() {
|
||||
const albumBlue = Color(0xff464566);
|
||||
|
||||
for (final blur in [false, true]) {
|
||||
testWidgets('active edge icons and tabs stay synchronized (glass: $blur)', (
|
||||
tester,
|
||||
) async {
|
||||
final activeTab = ValueNotifier(0);
|
||||
addTearDown(activeTab.dispose);
|
||||
await pumpShell(tester, blur: blur, activeTab: activeTab);
|
||||
final primary = MornyeTheme.build(Brightness.light).colorScheme.primary;
|
||||
Color? iconColor(String key, IconData data) {
|
||||
final icon = find.descendant(
|
||||
of: find.byKey(ValueKey(key)),
|
||||
matching: find.byIcon(data),
|
||||
);
|
||||
return IconTheme.of(tester.element(icon)).color;
|
||||
}
|
||||
|
||||
for (final (index, label) in [
|
||||
(1, 'Library'),
|
||||
(3, 'Search'),
|
||||
(0, 'Home'),
|
||||
]) {
|
||||
await tester.tapAt(
|
||||
tester.getCenter(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.text(label),
|
||||
)
|
||||
.first,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(activeTab.value, index);
|
||||
final tabIcons = find.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.byIcon(index == 3 ? Icons.search : Icons.home),
|
||||
);
|
||||
// Edge icons stay in the glass bar's selected/unselected layers at
|
||||
// rest, so the pill can reveal their red state during a held drag.
|
||||
for (final icon in tabIcons.evaluate()) {
|
||||
for (final opacity in tester.widgetList<Opacity>(
|
||||
find.ancestor(
|
||||
of: find.byElementPredicate((e) => e == icon),
|
||||
matching: find.byType(Opacity),
|
||||
),
|
||||
)) {
|
||||
expect(opacity.opacity, 1);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
iconColor('mornye-compact-home', Icons.home),
|
||||
index == 0 ? primary : isNot(primary),
|
||||
);
|
||||
expect(
|
||||
iconColor('mornye-compact-search', Icons.search),
|
||||
index == 3 ? primary : isNot(primary),
|
||||
);
|
||||
chrome.value = true;
|
||||
await tester.pumpAndSettle();
|
||||
chrome.expand();
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<MornyeTabBar>(find.byType(MornyeTabBar)).selectedIndex,
|
||||
index,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'edge icons travel continuously without fading (glass: $blur)',
|
||||
(tester) async {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
const channel = MethodChannel('com.zarz.spotiflac/backend');
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
|
||||
for (final scenario in ['fallback', 'local', 'empty', 'unavailable']) {
|
||||
testWidgets('metadata reads local lyrics: $scenario', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
const path = 'content://library/document/song.flac';
|
||||
const lyrics = '[00:01.00]A locally stored lyric line';
|
||||
var metadataReads = 0;
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'safStat':
|
||||
return jsonEncode({'exists': true, 'size': 100});
|
||||
case 'readAudioMetadata':
|
||||
return '{}';
|
||||
case 'getLyricsLRCWithSource':
|
||||
// Opening metadata must never trigger an online lyrics request.
|
||||
expect((call.arguments as Map)['file_path'], path);
|
||||
if (scenario == 'unavailable') {
|
||||
throw PlatformException(code: 'backend_unavailable');
|
||||
}
|
||||
return jsonEncode({
|
||||
'lyrics': scenario == 'local' ? lyrics : '',
|
||||
'source': scenario == 'local' ? 'Embedded' : '',
|
||||
});
|
||||
case 'readFileMetadata':
|
||||
expect((call.arguments as Map)['file_path'], path);
|
||||
metadataReads++;
|
||||
return jsonEncode({'lyrics': scenario == 'empty' ? '' : lyrics});
|
||||
case 'getSafFileModTimes':
|
||||
return '{}';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
||||
final item = DownloadHistoryItem(
|
||||
id: 'local-song',
|
||||
trackName: 'Song',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
filePath: path,
|
||||
service: 'example-provider',
|
||||
downloadedAt: DateTime(2026),
|
||||
format: 'flac',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: TrackMetadataScreen(item: item),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(metadataReads, scenario == 'local' ? 0 : 1);
|
||||
expect(
|
||||
find.text('A locally stored lyric line'),
|
||||
scenario == 'empty' ? findsNothing : findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text('No lyrics found in this file'),
|
||||
scenario == 'empty' ? findsOneWidget : findsNothing,
|
||||
);
|
||||
expect(find.text('Embed Lyrics'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user