mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-27 20:02:01 +02:00
feat: add generic extension provider resolution, progress phases, and instrumental lyrics heuristic
- Replace hardcoded provider prefix checks with resolveEffectiveMetadataProvider using replacesBuiltInProviders manifest capability - Add preparing/downloading/finalizing progress status constants and SetItemPreparing/SetItemDownloading APIs - Expose setDownloadStatus to extension JS runtime for fine-grained progress control - Skip lyrics search for instrumental tracks detected by title heuristic - Pass tidal/qobuz IDs to extension checkAvailability for richer matching - Add shouldAbortCancelledFallback helper for robust cancellation propagation - Add resolvePreferredTrackIDForExtension for intelligent track ID selection per extension - Remove ambiguous Auto/Default search provider option, always resolve to concrete provider - Add tests for shouldAbortCancelledFallback and progress status transitions
This commit is contained in:
@@ -1711,6 +1711,21 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status == 'preparing') {
|
||||
progressUpdates[itemId] = const _ProgressUpdate(
|
||||
status: DownloadStatus.downloading,
|
||||
progress: 0.0,
|
||||
speedMBps: 0,
|
||||
bytesReceived: 0,
|
||||
bytesTotal: 0,
|
||||
);
|
||||
|
||||
if (LogBuffer.loggingEnabled) {
|
||||
_log.d('Preparing [$itemId]: waiting for real download bytes');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final progressFromBackend =
|
||||
(itemProgress['progress'] as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
@@ -1823,6 +1838,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final bytesReceived =
|
||||
(firstProgress['bytes_received'] as num?)?.toInt() ?? 0;
|
||||
final bytesTotal = (firstProgress['bytes_total'] as num?)?.toInt() ?? 0;
|
||||
final backendStatus = firstProgress['status'] as String? ?? 'downloading';
|
||||
|
||||
if (downloadingCount > 0 && firstDownloading != null) {
|
||||
final trackName = downloadingCount == 1
|
||||
@@ -1835,7 +1851,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
int notifProgress = bytesReceived;
|
||||
int notifTotal = bytesTotal;
|
||||
|
||||
if (bytesTotal <= 0) {
|
||||
if (backendStatus == 'preparing') {
|
||||
notifProgress = 0;
|
||||
notifTotal = 100;
|
||||
} else if (bytesTotal <= 0) {
|
||||
final progressPercent =
|
||||
(firstProgress['progress'] as num?)?.toDouble() ?? 0.0;
|
||||
notifProgress = (progressPercent * 100).toInt();
|
||||
@@ -2620,11 +2639,17 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
try {
|
||||
final colonIdx = track.id.indexOf(':');
|
||||
final provider = track.id.substring(0, colonIdx);
|
||||
final effectiveProvider = resolveEffectiveMetadataProvider(
|
||||
provider,
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
final providerTrackId = track.id.substring(colonIdx + 1);
|
||||
|
||||
_log.d('No ISRC, fetching from $provider API: $providerTrackId');
|
||||
_log.d(
|
||||
'No ISRC, fetching from ${effectiveProvider.isEmpty ? provider : effectiveProvider} API: $providerTrackId',
|
||||
);
|
||||
final providerData = await PlatformBridge.getProviderMetadata(
|
||||
provider,
|
||||
effectiveProvider.isEmpty ? provider : effectiveProvider,
|
||||
'track',
|
||||
providerTrackId,
|
||||
);
|
||||
@@ -2647,7 +2672,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
}
|
||||
|
||||
_log.d('Resolved ISRC from $provider: $resolvedIsrc');
|
||||
_log.d(
|
||||
'Resolved ISRC from ${effectiveProvider.isEmpty ? provider : effectiveProvider}: $resolvedIsrc',
|
||||
);
|
||||
|
||||
final updatedTrack = _copyTrackWithResolvedMetadata(
|
||||
track,
|
||||
@@ -2661,7 +2688,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
final deezerTrackId = await _searchDeezerTrackIdByIsrc(
|
||||
resolvedIsrc,
|
||||
lookupContext: '$provider ISRC',
|
||||
lookupContext:
|
||||
'${effectiveProvider.isEmpty ? provider : effectiveProvider} ISRC',
|
||||
itemId: itemId,
|
||||
);
|
||||
|
||||
|
||||
@@ -345,6 +345,50 @@ String resolveEffectiveDownloadService(
|
||||
'';
|
||||
}
|
||||
|
||||
String resolveEffectiveMetadataProvider(
|
||||
String requestedProvider,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
final normalizedRequested = requestedProvider.trim().toLowerCase();
|
||||
final builtInMetadataIds = extensionState.builtInProviders
|
||||
.where((provider) => provider.supportsMetadata)
|
||||
.map((provider) => provider.id.trim().toLowerCase())
|
||||
.where((providerId) => providerId.isNotEmpty)
|
||||
.toSet();
|
||||
final enabledMetadataExtensions = extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasMetadataProvider)
|
||||
.toList(growable: false);
|
||||
|
||||
if (normalizedRequested.isNotEmpty) {
|
||||
if (builtInMetadataIds.contains(normalizedRequested)) {
|
||||
return normalizedRequested;
|
||||
}
|
||||
|
||||
final matchingExtension = enabledMetadataExtensions
|
||||
.where((ext) => ext.id.trim().toLowerCase() == normalizedRequested)
|
||||
.firstOrNull;
|
||||
if (matchingExtension != null) {
|
||||
return matchingExtension.id;
|
||||
}
|
||||
|
||||
final replacementExtension = enabledMetadataExtensions
|
||||
.where(
|
||||
(ext) => ext.replacesBuiltInProviders.contains(normalizedRequested),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (replacementExtension != null) {
|
||||
return replacementExtension.id;
|
||||
}
|
||||
}
|
||||
|
||||
return enabledMetadataExtensions.firstOrNull?.id ??
|
||||
extensionState.builtInProviders
|
||||
.where((provider) => provider.supportsMetadata)
|
||||
.map((provider) => provider.id)
|
||||
.firstOrNull ??
|
||||
'';
|
||||
}
|
||||
|
||||
bool isDeezerCompatibleDownloadService(
|
||||
String service,
|
||||
ExtensionState extensionState,
|
||||
@@ -1024,6 +1068,23 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
String? _firstEnabledSearchProviderId() {
|
||||
return state.extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.searchBehavior?.primary == true,
|
||||
)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull ??
|
||||
state.extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull ??
|
||||
defaultBuiltInSearchProviderId;
|
||||
}
|
||||
|
||||
String? replacedBuiltInDownloadProviderFor(String providerId) {
|
||||
final normalized = providerId.trim().toLowerCase();
|
||||
if (normalized.isEmpty) return null;
|
||||
@@ -1137,8 +1198,18 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
void _reconcileSearchProvider() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final currentSearchProvider = settings.searchProvider?.trim();
|
||||
if (currentSearchProvider == null || currentSearchProvider.isEmpty) {
|
||||
final currentSearchProvider = settings.searchProvider?.trim() ?? '';
|
||||
final preferredSearchProvider = _firstEnabledSearchProviderId() ?? '';
|
||||
|
||||
if (currentSearchProvider.isEmpty) {
|
||||
if (preferredSearchProvider.isNotEmpty) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSearchProvider(preferredSearchProvider);
|
||||
_log.d(
|
||||
'Adopted first enabled search provider as default: $preferredSearchProvider',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1161,9 +1232,15 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
);
|
||||
if (!isBuiltInSearchProvider(currentSearchProvider) &&
|
||||
!hasMatchingExtension) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSearchProvider(
|
||||
preferredSearchProvider.isNotEmpty ? preferredSearchProvider : null,
|
||||
);
|
||||
_log.d(
|
||||
'Cleared stale search provider because $currentSearchProvider is no longer available',
|
||||
preferredSearchProvider.isNotEmpty
|
||||
? 'Reset stale search provider $currentSearchProvider to $preferredSearchProvider'
|
||||
: 'Cleared stale search provider because $currentSearchProvider is no longer available',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,32 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
int? _albumTotalTracks;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
String _legacyProviderIdFromResourceId(String value) {
|
||||
if (value.startsWith('deezer:')) return 'deezer';
|
||||
if (value.startsWith('qobuz:')) return 'qobuz';
|
||||
if (value.startsWith('tidal:')) return 'tidal';
|
||||
if (value.startsWith('spotify:')) return 'spotify';
|
||||
return 'spotify';
|
||||
}
|
||||
|
||||
String _effectiveMetadataProviderIdFromAlbumId() {
|
||||
if (widget.extensionId != null && widget.extensionId!.isNotEmpty) {
|
||||
return widget.extensionId!;
|
||||
}
|
||||
return resolveEffectiveMetadataProvider(
|
||||
_legacyProviderIdFromResourceId(widget.albumId),
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
}
|
||||
|
||||
String _stripPrefixedResourceId(String value) {
|
||||
final colonIndex = value.indexOf(':');
|
||||
if (colonIndex <= 0 || colonIndex == value.length - 1) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -87,14 +113,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final providerId =
|
||||
widget.extensionId ??
|
||||
(() {
|
||||
if (widget.albumId.startsWith('deezer:')) return 'deezer';
|
||||
if (widget.albumId.startsWith('qobuz:')) return 'qobuz';
|
||||
if (widget.albumId.startsWith('tidal:')) return 'tidal';
|
||||
return 'spotify';
|
||||
})();
|
||||
final providerId = _effectiveMetadataProviderIdFromAlbumId();
|
||||
ref
|
||||
.read(recentAccessProvider.notifier)
|
||||
.recordAlbumAccess(
|
||||
@@ -263,21 +282,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
}
|
||||
|
||||
String? _directMetadataProviderId() {
|
||||
if (widget.extensionId != null && widget.extensionId!.isNotEmpty) {
|
||||
return widget.extensionId;
|
||||
}
|
||||
if (widget.albumId.startsWith('deezer:')) return 'deezer';
|
||||
if (widget.albumId.startsWith('qobuz:')) return 'qobuz';
|
||||
if (widget.albumId.startsWith('tidal:')) return 'tidal';
|
||||
return null;
|
||||
final providerId = _effectiveMetadataProviderIdFromAlbumId();
|
||||
return providerId.isEmpty ? null : providerId;
|
||||
}
|
||||
|
||||
String _metadataResourceId(String providerId) {
|
||||
final prefixed = '$providerId:';
|
||||
if (widget.albumId.startsWith(prefixed)) {
|
||||
return widget.albumId.substring(prefixed.length);
|
||||
}
|
||||
return widget.albumId;
|
||||
return _stripPrefixedResourceId(widget.albumId);
|
||||
}
|
||||
|
||||
Track _parseTrack(
|
||||
@@ -656,9 +666,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, service);
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, service);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
|
||||
@@ -158,22 +158,39 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
return _directMetadataProviderId();
|
||||
}
|
||||
|
||||
String? _directMetadataProviderId() {
|
||||
String _legacyProviderIdFromResourceId(String value) {
|
||||
if (value.startsWith('deezer:')) return 'deezer';
|
||||
if (value.startsWith('qobuz:')) return 'qobuz';
|
||||
if (value.startsWith('tidal:')) return 'tidal';
|
||||
if (value.startsWith('spotify:')) return 'spotify';
|
||||
return 'spotify';
|
||||
}
|
||||
|
||||
String _effectiveMetadataProviderIdFromArtistId() {
|
||||
if (widget.extensionId != null && widget.extensionId!.isNotEmpty) {
|
||||
return widget.extensionId;
|
||||
return widget.extensionId!;
|
||||
}
|
||||
if (widget.artistId.startsWith('deezer:')) return 'deezer';
|
||||
if (widget.artistId.startsWith('qobuz:')) return 'qobuz';
|
||||
if (widget.artistId.startsWith('tidal:')) return 'tidal';
|
||||
return null;
|
||||
return resolveEffectiveMetadataProvider(
|
||||
_legacyProviderIdFromResourceId(widget.artistId),
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
}
|
||||
|
||||
String _stripPrefixedResourceId(String value) {
|
||||
final colonIndex = value.indexOf(':');
|
||||
if (colonIndex <= 0 || colonIndex == value.length - 1) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
String? _directMetadataProviderId() {
|
||||
final providerId = _effectiveMetadataProviderIdFromArtistId();
|
||||
return providerId.isEmpty ? null : providerId;
|
||||
}
|
||||
|
||||
String _metadataResourceId(String providerId) {
|
||||
final prefixed = '$providerId:';
|
||||
if (widget.artistId.startsWith(prefixed)) {
|
||||
return widget.artistId.substring(prefixed.length);
|
||||
}
|
||||
return widget.artistId;
|
||||
return _stripPrefixedResourceId(widget.artistId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -183,14 +200,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final providerId =
|
||||
widget.extensionId ??
|
||||
(() {
|
||||
if (widget.artistId.startsWith('deezer:')) return 'deezer';
|
||||
if (widget.artistId.startsWith('qobuz:')) return 'qobuz';
|
||||
if (widget.artistId.startsWith('tidal:')) return 'tidal';
|
||||
return 'spotify';
|
||||
})();
|
||||
final providerId = _effectiveMetadataProviderIdFromArtistId();
|
||||
ref
|
||||
.read(recentAccessProvider.notifier)
|
||||
.recordArtistAccess(
|
||||
@@ -1034,9 +1044,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
Future<List<Track>> _fetchAlbumTracks(ArtistAlbum album) async {
|
||||
final providerId = album.providerId;
|
||||
if (providerId != null && providerId.isNotEmpty) {
|
||||
final resourceId = album.id.startsWith('$providerId:')
|
||||
? album.id.substring(providerId.length + 1)
|
||||
: album.id;
|
||||
final resourceId = _stripPrefixedResourceId(album.id);
|
||||
final metadata = await PlatformBridge.getProviderMetadata(
|
||||
providerId,
|
||||
'album',
|
||||
|
||||
@@ -3643,20 +3643,17 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.toList();
|
||||
final builtInProviders = builtInSearchProviderSpecs;
|
||||
final primarySearchExtension = _defaultSearchExtension(searchProviders);
|
||||
final defaultProviderTarget =
|
||||
primarySearchExtension?.displayName ??
|
||||
defaultBuiltInSearchProviderDisplayName ??
|
||||
context.l10n.extensionDefaultProvider;
|
||||
final defaultProviderLabel =
|
||||
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
|
||||
final defaultProviderIconPath = primarySearchExtension?.iconPath;
|
||||
final currentProvider =
|
||||
final resolvedCurrentProvider =
|
||||
rawCurrentProvider != null &&
|
||||
rawCurrentProvider.isNotEmpty &&
|
||||
(isBuiltInSearchProvider(rawCurrentProvider) ||
|
||||
searchProviders.any((e) => e.id == rawCurrentProvider))
|
||||
? rawCurrentProvider
|
||||
: _defaultSearchExtension(searchProviders)?.id ??
|
||||
defaultBuiltInSearchProviderId;
|
||||
final currentProvider =
|
||||
resolvedCurrentProvider != null && resolvedCurrentProvider.isNotEmpty
|
||||
? resolvedCurrentProvider
|
||||
: null;
|
||||
|
||||
Extension? currentExt;
|
||||
@@ -3676,19 +3673,6 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
if (currentExt.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!);
|
||||
}
|
||||
} else if (primarySearchExtension?.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(
|
||||
primarySearchExtension!.searchBehavior!.icon!,
|
||||
);
|
||||
iconPath = defaultProviderIconPath;
|
||||
} else if (defaultProviderIconPath != null &&
|
||||
defaultProviderIconPath.isNotEmpty) {
|
||||
iconPath = defaultProviderIconPath;
|
||||
if (primarySearchExtension?.searchBehavior?.icon != null) {
|
||||
displayIcon = _getIconFromName(
|
||||
primarySearchExtension!.searchBehavior!.icon!,
|
||||
);
|
||||
}
|
||||
} else if (isBuiltInProvider) {
|
||||
displayIcon = resolveProviderIcon(currentProvider);
|
||||
}
|
||||
@@ -3724,39 +3708,10 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
offset: const Offset(0, 40),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onSelected: (String providerId) {
|
||||
final provider = providerId.isEmpty ? null : providerId;
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(provider);
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(providerId);
|
||||
onProviderChanged?.call();
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: '',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.music_note,
|
||||
size: 20,
|
||||
color: currentProvider == null || currentProvider.isEmpty
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
defaultProviderLabel,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
currentProvider == null || currentProvider.isEmpty
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentProvider == null || currentProvider.isEmpty)
|
||||
Icon(Icons.check, size: 18, color: colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
...builtInProviders.map(
|
||||
(provider) => PopupMenuItem<String>(
|
||||
value: provider.id,
|
||||
|
||||
@@ -52,19 +52,33 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
String get _playlistName => _resolvedPlaylistName ?? widget.playlistName;
|
||||
String? get _coverUrl => _resolvedCoverUrl ?? widget.coverUrl;
|
||||
|
||||
String? _metadataProviderId(String playlistId) {
|
||||
if (playlistId.startsWith('deezer:')) return 'deezer';
|
||||
if (playlistId.startsWith('qobuz:')) return 'qobuz';
|
||||
if (playlistId.startsWith('tidal:')) return 'tidal';
|
||||
String? _legacyProviderIdFromResourceId(String value) {
|
||||
if (value.startsWith('deezer:')) return 'deezer';
|
||||
if (value.startsWith('qobuz:')) return 'qobuz';
|
||||
if (value.startsWith('tidal:')) return 'tidal';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _metadataResourceId(String providerId, String playlistId) {
|
||||
final prefixed = '$providerId:';
|
||||
if (playlistId.startsWith(prefixed)) {
|
||||
return playlistId.substring(prefixed.length);
|
||||
String _stripPrefixedResourceId(String value) {
|
||||
final colonIndex = value.indexOf(':');
|
||||
if (colonIndex <= 0 || colonIndex == value.length - 1) {
|
||||
return value;
|
||||
}
|
||||
return playlistId;
|
||||
return value.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
String? _metadataProviderId(String playlistId) {
|
||||
final providerId = _legacyProviderIdFromResourceId(playlistId);
|
||||
if (providerId == null) return null;
|
||||
final effective = resolveEffectiveMetadataProvider(
|
||||
providerId,
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
return effective.isEmpty ? null : effective;
|
||||
}
|
||||
|
||||
String _metadataResourceId(String providerId, String playlistId) {
|
||||
return _stripPrefixedResourceId(playlistId);
|
||||
}
|
||||
|
||||
String? _recommendedDownloadService() {
|
||||
@@ -75,8 +89,13 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
|
||||
final playlistId = widget.playlistId;
|
||||
if (playlistId != null) {
|
||||
if (playlistId.startsWith('tidal:')) return 'tidal';
|
||||
if (playlistId.startsWith('qobuz:')) return 'qobuz';
|
||||
final providerId = _metadataProviderId(playlistId);
|
||||
if (providerId != null && providerId.isNotEmpty) {
|
||||
return resolveEffectiveDownloadService(
|
||||
providerId,
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final source = _tracks.firstOrNull?.source;
|
||||
@@ -85,8 +104,13 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
}
|
||||
|
||||
final trackId = _tracks.firstOrNull?.id ?? '';
|
||||
if (trackId.startsWith('tidal:')) return 'tidal';
|
||||
if (trackId.startsWith('qobuz:')) return 'qobuz';
|
||||
final trackProviderId = _legacyProviderIdFromResourceId(trackId);
|
||||
if (trackProviderId != null) {
|
||||
return resolveEffectiveDownloadService(
|
||||
trackProviderId,
|
||||
ref.read(extensionProvider),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -509,11 +533,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
}
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(
|
||||
track,
|
||||
service,
|
||||
playlistName: _playlistName,
|
||||
);
|
||||
.addToQueue(track, service, playlistName: _playlistName);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
|
||||
@@ -674,18 +674,19 @@ class _SearchProviderSelector extends ConsumerWidget {
|
||||
final hasAnyProvider =
|
||||
searchProviders.isNotEmpty || builtInProviders.isNotEmpty;
|
||||
|
||||
String currentProviderName = context.l10n.extensionDefaultProvider;
|
||||
if (settings.searchProvider != null &&
|
||||
settings.searchProvider!.isNotEmpty) {
|
||||
if (isBuiltInSearchProvider(settings.searchProvider)) {
|
||||
currentProviderName = resolveProviderDisplayName(
|
||||
settings.searchProvider!,
|
||||
);
|
||||
final resolvedProviderId =
|
||||
(settings.searchProvider != null && settings.searchProvider!.isNotEmpty)
|
||||
? settings.searchProvider!
|
||||
: searchProviders.firstOrNull?.id ?? defaultBuiltInSearchProviderId;
|
||||
String currentProviderName = context.l10n.optionsPrimaryProviderSubtitle;
|
||||
if (resolvedProviderId != null && resolvedProviderId.isNotEmpty) {
|
||||
if (isBuiltInSearchProvider(resolvedProviderId)) {
|
||||
currentProviderName = resolveProviderDisplayName(resolvedProviderId);
|
||||
} else {
|
||||
final ext = searchProviders
|
||||
.where((e) => e.id == settings.searchProvider)
|
||||
.where((e) => e.id == resolvedProviderId)
|
||||
.firstOrNull;
|
||||
currentProviderName = ext?.displayName ?? settings.searchProvider!;
|
||||
currentProviderName = ext?.displayName ?? resolvedProviderId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,20 +789,6 @@ class _SearchProviderSelector extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.music_note, color: colorScheme.primary),
|
||||
title: Text(ctx.l10n.extensionDefaultProvider),
|
||||
subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle),
|
||||
trailing:
|
||||
(settings.searchProvider == null ||
|
||||
settings.searchProvider!.isEmpty)
|
||||
? Icon(Icons.check_circle, color: colorScheme.primary)
|
||||
: Icon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
...builtInProviders.map(
|
||||
(provider) => ListTile(
|
||||
leading: Icon(Icons.search, color: colorScheme.tertiary),
|
||||
|
||||
@@ -741,13 +741,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
|
||||
final rawSearchProvider = settings.searchProvider?.trim() ?? '';
|
||||
final isValidBuiltIn = isBuiltInSearchProvider(rawSearchProvider);
|
||||
final primarySearchExtension = _defaultSearchExtension(extState.extensions);
|
||||
final defaultProviderTarget =
|
||||
primarySearchExtension?.displayName ??
|
||||
defaultBuiltInSearchProviderDisplayName ??
|
||||
context.l10n.extensionDefaultProvider;
|
||||
final defaultProviderLabel =
|
||||
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
|
||||
final searchProvider =
|
||||
isValidBuiltIn ||
|
||||
extState.extensions.any(
|
||||
@@ -755,7 +748,9 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
e.enabled && e.hasCustomSearch && e.id == rawSearchProvider,
|
||||
)
|
||||
? rawSearchProvider
|
||||
: '';
|
||||
: _defaultSearchExtension(extState.extensions)?.id ??
|
||||
defaultBuiltInSearchProviderId ??
|
||||
'';
|
||||
final isBuiltIn = isBuiltInSearchProvider(searchProvider);
|
||||
|
||||
Extension? activeExtension;
|
||||
@@ -764,7 +759,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
.where((e) => e.id == searchProvider && e.enabled)
|
||||
.firstOrNull;
|
||||
}
|
||||
final hasNonDefaultProvider = isBuiltIn || activeExtension != null;
|
||||
|
||||
String subtitle;
|
||||
if (isBuiltIn) {
|
||||
@@ -792,7 +786,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: hasNonDefaultProvider
|
||||
color: searchProvider.isNotEmpty
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -802,16 +796,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_SourceChip(
|
||||
icon: Icons.graphic_eq,
|
||||
label: defaultProviderLabel,
|
||||
isSelected: searchProvider.isEmpty,
|
||||
onTap: () {
|
||||
if (hasNonDefaultProvider) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
}
|
||||
},
|
||||
),
|
||||
for (final provider in builtInProviders)
|
||||
_SourceChip(
|
||||
icon: resolveProviderIcon(
|
||||
@@ -840,7 +824,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tap $defaultProviderLabel to switch back from extension',
|
||||
context.l10n.optionsSwitchBack,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user