mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
fix(search): expose go to album for sparse tracks
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
@@ -282,6 +283,91 @@ Future<void> navigateToAlbum(
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the album for a track, enriching sparse search results first when
|
||||
/// they do not include album identity. This keeps collection actions generic:
|
||||
/// any metadata extension can supply the missing track fields through search.
|
||||
Future<void> navigateToTrackAlbum(BuildContext context, Track track) async {
|
||||
final albumName = track.albumName.trim();
|
||||
final albumId = track.albumId?.trim();
|
||||
final hasAlbumId = albumId != null && !_isUnknownResourceId(albumId);
|
||||
|
||||
if (albumName.isNotEmpty || hasAlbumId) {
|
||||
await navigateToAlbum(
|
||||
context,
|
||||
albumName: albumName.isNotEmpty ? albumName : context.l10n.trackAlbum,
|
||||
albumId: hasAlbumId ? albumId : null,
|
||||
artistName: _preferredAlbumArtist(track),
|
||||
coverUrl: track.coverUrl,
|
||||
extensionId: track.source,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final query = [
|
||||
track.name.trim(),
|
||||
track.artistName.trim(),
|
||||
].where((part) => part.isNotEmpty).join(' ');
|
||||
if (query.isEmpty) {
|
||||
_showUnavailable(context, context.l10n.trackAlbum);
|
||||
return;
|
||||
}
|
||||
|
||||
_showLoadingSnackBar(context, 'Looking up album...');
|
||||
try {
|
||||
final searchResult = await _searchMetadataProviders(
|
||||
context,
|
||||
query,
|
||||
filter: 'track',
|
||||
limit: 20,
|
||||
sourceProviderId: track.source,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
final matches = searchResult?.items ?? const <Map<String, dynamic>>[];
|
||||
final bestMatch = _pickBestResultByName(matches, track.name);
|
||||
if (bestMatch == null) {
|
||||
_showUnavailable(context, context.l10n.trackAlbum);
|
||||
return;
|
||||
}
|
||||
|
||||
final providerId = _resolveResultProviderId(
|
||||
bestMatch,
|
||||
searchResult?.providerId,
|
||||
);
|
||||
final resolvedTrack = Track.fromBackendMap(bestMatch, source: providerId);
|
||||
final resolvedAlbumName = resolvedTrack.albumName.trim();
|
||||
final resolvedAlbumId = resolvedTrack.albumId?.trim();
|
||||
final hasResolvedAlbumId =
|
||||
resolvedAlbumId != null && !_isUnknownResourceId(resolvedAlbumId);
|
||||
if (resolvedAlbumName.isEmpty && !hasResolvedAlbumId) {
|
||||
_showUnavailable(context, context.l10n.trackAlbum);
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateToAlbum(
|
||||
context,
|
||||
albumName: resolvedAlbumName.isNotEmpty
|
||||
? resolvedAlbumName
|
||||
: context.l10n.trackAlbum,
|
||||
albumId: hasResolvedAlbumId ? resolvedAlbumId : null,
|
||||
artistName: _preferredAlbumArtist(resolvedTrack),
|
||||
coverUrl: resolvedTrack.coverUrl ?? track.coverUrl,
|
||||
extensionId: providerId,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.e('Failed to resolve album for track "${track.name}": $e', e);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
_showUnavailable(context, context.l10n.trackAlbum);
|
||||
}
|
||||
}
|
||||
|
||||
String _preferredAlbumArtist(Track track) {
|
||||
final albumArtist = (track.albumArtist ?? '').trim();
|
||||
return albumArtist.isNotEmpty ? albumArtist : track.artistName;
|
||||
}
|
||||
|
||||
void _pushArtistScreen(
|
||||
BuildContext context, {
|
||||
required String artistId,
|
||||
|
||||
@@ -194,12 +194,11 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
onTap: () => _downloadQualityVariant(context, ref),
|
||||
),
|
||||
|
||||
if (track.albumName.trim().isNotEmpty)
|
||||
_OptionTile(
|
||||
icon: Icons.album_outlined,
|
||||
title: context.l10n.homeGoToAlbum,
|
||||
onTap: () => _goToAlbum(context),
|
||||
),
|
||||
_OptionTile(
|
||||
icon: Icons.album_outlined,
|
||||
title: context.l10n.homeGoToAlbum,
|
||||
onTap: () => _goToAlbum(context),
|
||||
),
|
||||
|
||||
_OptionTile(
|
||||
icon: isLoved ? Icons.favorite : Icons.favorite_border,
|
||||
@@ -281,16 +280,8 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).context;
|
||||
final albumArtist = (track.albumArtist ?? '').trim();
|
||||
Navigator.pop(context);
|
||||
await navigateToAlbum(
|
||||
navigationContext,
|
||||
albumName: track.albumName,
|
||||
albumId: track.albumId,
|
||||
artistName: albumArtist.isNotEmpty ? albumArtist : track.artistName,
|
||||
coverUrl: track.coverUrl,
|
||||
extensionId: track.source,
|
||||
);
|
||||
await navigateToTrackAlbum(navigationContext, track);
|
||||
}
|
||||
|
||||
Future<void> _playLocal(BuildContext context, WidgetRef ref) async {
|
||||
|
||||
@@ -20,6 +20,14 @@ void main() {
|
||||
previewUrl: 'https://example.com/preview.mp3',
|
||||
duration: 180,
|
||||
);
|
||||
const sparseSearchTrack = Track(
|
||||
id: 'search-track',
|
||||
name: 'Search Track',
|
||||
artistName: 'Artist',
|
||||
albumName: '',
|
||||
duration: 180,
|
||||
source: 'metadata-extension',
|
||||
);
|
||||
|
||||
Future<void> pumpButton(WidgetTester tester, {MediaItem? currentItem}) async {
|
||||
await tester.pumpWidget(
|
||||
@@ -113,6 +121,35 @@ void main() {
|
||||
expect(find.text('Go to Album'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.album_outlined), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'search track options keep Go to Album when album data is sparse',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
settingsProvider.overrideWith(_TestSettingsNotifier.new),
|
||||
libraryCollectionsProvider.overrideWith(
|
||||
_TestLibraryCollectionsNotifier.new,
|
||||
),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: TrackCollectionQuickActions(track: sparseSearchTrack),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Go to Album'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.album_outlined), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _TestSettingsNotifier extends SettingsNotifier {
|
||||
|
||||
Reference in New Issue
Block a user