mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-28 20:32:15 +02:00
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:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user