fix(ui): resolve album titles and show Home explicit badges

Hide the profile setup hint once a name or photo is saved.
This commit is contained in:
zarzet
2026-09-21 19:02:13 +07:00
parent 5958fe2719
commit 4c5af08cdd
7 changed files with 145 additions and 13 deletions
+5
View File
@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
@@ -20,6 +21,7 @@ class ExploreItem {
final String? coverUrl;
final String? featuredCoverUrl;
final String? heading;
final bool? explicit;
final String? providerId;
final String? albumId;
final String? albumName;
@@ -36,6 +38,7 @@ class ExploreItem {
this.coverUrl,
this.featuredCoverUrl,
this.heading,
this.explicit,
this.providerId,
this.albumId,
this.albumName,
@@ -54,6 +57,7 @@ class ExploreItem {
coverUrl: json['cover_url'] as String?,
featuredCoverUrl: json['featured_cover_url'] as String?,
heading: json['heading'] as String?,
explicit: parseExplicitFlag(json['explicit']),
providerId: json['provider_id'] as String?,
albumId: json['album_id'] as String?,
albumName: json['album_name'] as String?,
@@ -72,6 +76,7 @@ class ExploreItem {
'cover_url': coverUrl,
'featured_cover_url': featuredCoverUrl,
'heading': heading,
'explicit': explicit,
'provider_id': providerId,
'album_id': albumId,
'album_name': albumName,
+7 -4
View File
@@ -233,8 +233,9 @@ extension _HomeTabExploreUI on _HomeTabState {
),
),
const SizedBox(height: 8),
Text(
item.name,
ExplicitTrackTitle(
title: item.name,
explicit: item.explicit == true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: isArtist ? TextAlign.center : TextAlign.start,
@@ -414,8 +415,9 @@ extension _HomeTabExploreUI on _HomeTabState {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
ExplicitTrackTitle(
title: item.name,
explicit: item.explicit == true,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
maxLines: 2,
@@ -478,6 +480,7 @@ extension _HomeTabExploreUI on _HomeTabState {
releaseDate: item.releaseDate,
coverUrl: item.coverUrl,
source: _providerIdForExploreItem(item),
explicit: item.explicit,
);
if (settings.askQualityBeforeDownload || settings.allowQualityVariants) {
+15 -4
View File
@@ -1022,6 +1022,7 @@ class ExtensionAlbumScreen extends ConsumerStatefulWidget {
class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
List<Track>? _tracks;
String? _albumName;
bool _isLoading = true;
String? _error;
String? _artistId;
@@ -1074,6 +1075,9 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
final artistId = (albumInfo['artist_id'] ?? albumInfo['artistId'])
?.toString();
final albumName =
normalizeOptionalString(albumInfo['name']?.toString()) ??
widget.albumName;
final artistName = (albumInfo['artists'] ?? albumInfo['artist'])
?.toString();
final albumType =
@@ -1090,6 +1094,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
.map(
(t) => _parseTrack(
t as Map<String, dynamic>,
albumNameFallback: albumName,
albumTypeFallback: albumType,
totalTracksFallback: totalTracks,
),
@@ -1098,6 +1103,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
setState(() {
_tracks = tracks;
_albumName = albumName;
_artistId = artistId;
_artistName = artistName;
_albumType = albumType;
@@ -1124,13 +1130,17 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
Track _parseTrack(
Map<String, dynamic> data, {
String? albumNameFallback,
String? albumTypeFallback,
int? totalTracksFallback,
}) {
final base = Track.fromBackendMap(data, source: widget.extensionId);
return base.copyWith(
id: (data['id'] ?? '').toString(),
albumName: (data['album_name'] ?? widget.albumName).toString(),
albumName:
normalizeOptionalString(data['album_name']?.toString()) ??
albumNameFallback ??
widget.albumName,
albumArtist: normalizeOptionalString(data['album_artist']?.toString()),
artistId: base.artistId ?? _artistId,
albumId: base.albumId ?? widget.albumId,
@@ -1163,7 +1173,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
return AlbumScreen(
albumId: widget.albumId,
albumName: widget.albumName,
albumName: _albumName ?? widget.albumName,
coverUrl: widget.coverUrl,
headerVideoUrl: _headerVideoUrl,
headerImageUrl: _headerImageUrl,
@@ -1638,8 +1648,9 @@ class _QuickPicksPageViewState extends State<_QuickPicksPageView> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.name,
ExplicitTrackTitle(
title: item.name,
explicit: item.explicit == true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+5 -1
View File
@@ -441,7 +441,11 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(context.l10n.profileEdit),
subtitle:
profile?.name.isNotEmpty == true ||
profile?.photoPath?.isNotEmpty == true
? null
: Text(context.l10n.profileEdit),
trailing: const Icon(Icons.chevron_right),
onTap: profile == null
? null
+4 -2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/providers/explore_provider.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
/// Large editorial cards requested by a home-feed provider.
class ExploreFeaturedSection extends StatelessWidget {
@@ -92,8 +93,9 @@ class ExploreFeaturedSection extends StatelessWidget {
),
SizedBox(
height: titleHeight,
child: Text(
item.name,
child: ExplicitTrackTitle(
title: item.name,
explicit: item.explicit == true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
+87
View File
@@ -8,11 +8,98 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/l10n/app_localizations.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/screens/album_screen.dart';
import 'package:spotiflac_android/screens/home_tab.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/track_list_tile.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const backendChannel = MethodChannel('com.zarz.spotiflac/backend');
final backendMessenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
setUp(() async {
SharedPreferences.setMockInitialValues({});
backendMessenger.setMockMethodCallHandler(
backendChannel,
(_) async => null,
);
await PlatformBridge.clearTrackCache();
});
tearDown(() async {
backendMessenger.setMockMethodCallHandler(
backendChannel,
(_) async => null,
);
await PlatformBridge.clearTrackCache();
backendMessenger.setMockMethodCallHandler(backendChannel, null);
});
for (final nested in [true, false]) {
for (final fetchedName in ['Actual Album Title', ' ']) {
testWidgets(
'extension album resolves its title (nested: $nested, name: "$fetchedName")',
(tester) async {
SharedPreferences.setMockInitialValues({});
await tester.binding.setSurfaceSize(const Size(430, 1200));
addTearDown(() => tester.binding.setSurfaceSize(null));
const channel = MethodChannel('com.zarz.spotiflac/backend');
final messenger = tester.binding.defaultBinaryMessenger;
var requests = 0;
messenger.setMockMethodCallHandler(channel, (call) async {
if (call.method != 'getProviderMetadata') return null;
requests++;
final info = <String, dynamic>{
'name': fetchedName,
'artists': 'Example Artist',
'total_tracks': 1,
};
final tracks = [
{
'id': 'example-song',
'name': 'Example Song',
'artists': 'Example Artist',
'album_name': '',
'duration_ms': 180000,
},
];
return jsonEncode(
nested
? {'album_info': info, 'track_list': tracks}
: {...info, 'tracks': tracks},
);
});
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.light),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const ExtensionAlbumScreen(
extensionId: 'example-metadata',
albumId: 'example-album',
albumName: 'Album',
),
),
),
);
await tester.pumpAndSettle();
final expected = fetchedName.trim().isEmpty ? 'Album' : fetchedName;
final album = tester.widget<AlbumScreen>(find.byType(AlbumScreen));
expect(album.albumName, expected);
expect(album.tracks!.single.albumName, expected);
expect(find.text(expected), findsWidgets);
expect(requests, 1);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
await tester.pumpAndSettle();
},
);
}
}
testWidgets('album tracks retain the extended tags supplied in search', (
tester,
) async {
+22 -2
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/providers/explore_provider.dart';
import 'package:spotiflac_android/widgets/explore_featured_section.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/l10n/app_localizations.dart';
void main() {
test('featured layout and artwork survive cache serialization', () {
@@ -18,6 +20,7 @@ void main() {
'featured_cover_url': 'https://example.test/banner.jpg',
'heading': 'Updated playlist',
'provider_id': 'example-provider',
'explicit': true,
},
],
});
@@ -27,6 +30,9 @@ void main() {
expect(restored.items.single.featuredCoverUrl, endsWith('/banner.jpg'));
expect(restored.items.single.coverUrl, endsWith('/cover.jpg'));
expect(restored.items.single.providerId, 'example-provider');
expect(restored.items.single.explicit, isTrue);
expect(ExploreItem.fromJson({'explicit': false}).explicit, isFalse);
expect(ExploreItem.fromJson({}).explicit, isNull);
expect(ExploreSection.fromJson({'items': <Object?>[]}).isFeatured, isFalse);
});
@@ -51,11 +57,14 @@ void main() {
name: 'Featured $index',
artists: 'Example artist',
heading: 'New album',
explicit: index == 0,
description: 'An editorial description of the featured album.',
),
);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: MediaQuery(
data: MediaQueryData(textScaler: TextScaler.linear(scale)),
child: Scaffold(
@@ -75,12 +84,23 @@ void main() {
),
);
expect(tester.takeException(), isNull);
expect(find.byType(ExplicitBadge), findsOneWidget);
expect(
tester
.widget<ExplicitTrackTitle>(find.byType(ExplicitTrackTitle).first)
.explicit,
isTrue,
);
final firstTitle = find.byWidgetPredicate(
(widget) =>
widget is ExplicitTrackTitle && widget.title == 'Featured 0',
);
final card = find.ancestor(
of: find.text('Featured 0'),
of: firstTitle,
matching: find.byType(GestureDetector),
);
expect(tester.getSize(card).width, greaterThan(250));
await tester.tap(find.text('Featured 0'));
await tester.tap(firstTitle);
expect(opened?.id, 'collection-0');
await tester.drag(find.byType(ListView), const Offset(-600, 0));
await tester.pumpAndSettle();