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:
zarzet committed 2026-09-21 18:07:15 +07:00
1 parent d8d46108bc
commit 1c535b493c
7 files changed
+217 -37

No files matched your search

+77 -6
View File
@@ -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 {
+84
View File
@@ -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);
});
}
}