feat: expose audio duration in metadata API and fix home empty-state race

- Add Duration field to AudioQuality for FLAC (streaminfo) and M4A (mvhd atom)
- Expose duration via ReadFileMetadata and extension runtime Go-backend API
- Pass duration_ms to extension CheckAvailability for better track matching
- Fix home tab showing empty state before extensions finish initializing by
  keeping the search bar visible with a loading indicator until ready
- Refactor hasSearchProvider helper to account for built-in providers
- Refine homeEmptyTitle/Subtitle copy (EN + ID translations)
- Bump version to 4.5.0+127
This commit is contained in:
zarzet
2026-04-24 04:38:41 +07:00
parent bb7c86c29e
commit cd2c2a9854
23 changed files with 277 additions and 110 deletions
+1
View File
@@ -1397,6 +1397,7 @@ func ReadFileMetadata(filePath string) (string, error) {
if qualityErr == nil {
result["bit_depth"] = quality.BitDepth
result["sample_rate"] = quality.SampleRate
result["duration"] = quality.Duration
}
} else if isMp3 {
meta, err := ReadID3Tags(filePath)
+7 -6
View File
@@ -941,10 +941,10 @@ func (p *extensionProviderWrapper) EnrichTrackForItemID(track *ExtTrackMetadata,
}
func (p *extensionProviderWrapper) CheckAvailability(isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID string) (*ExtAvailabilityResult, error) {
return p.CheckAvailabilityForItemID(isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID, "")
return p.CheckAvailabilityForItemID(isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID, 0, "")
}
func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID string, itemID string) (*ExtAvailabilityResult, error) {
func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID string, durationMS int, itemID string) (*ExtAvailabilityResult, error) {
if !p.extension.Manifest.IsDownloadProvider() {
return nil, fmt.Errorf("extension '%s' is not a download provider", p.extension.ID)
}
@@ -975,12 +975,13 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a
spotify_id: %q,
deezer_id: %q,
tidal_id: %q,
qobuz_id: %q
qobuz_id: %q,
duration_ms: %d
});
}
return null;
})()
`, isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID)
`, isrc, trackName, artistName, spotifyID, deezerID, tidalID, qobuzID, durationMS)
result, err := RunWithTimeoutAndRecover(p.vm, script, DefaultJSTimeout)
if err != nil {
@@ -1707,7 +1708,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
ext, err := extManager.GetExtension(req.Source)
if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() {
provider := newExtensionProviderWrapper(ext)
availability, availErr := provider.CheckAvailabilityForItemID(req.ISRC, req.TrackName, req.ArtistName, req.SpotifyID, req.DeezerID, req.TidalID, req.QobuzID, req.ItemID)
availability, availErr := provider.CheckAvailabilityForItemID(req.ISRC, req.TrackName, req.ArtistName, req.SpotifyID, req.DeezerID, req.TidalID, req.QobuzID, req.DurationMS, req.ItemID)
if shouldAbortCancelledFallback(req.ItemID, availErr) {
return nil, ErrDownloadCancelled
}
@@ -2119,7 +2120,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
provider := newExtensionProviderWrapper(ext)
availability, err := provider.CheckAvailabilityForItemID(req.ISRC, req.TrackName, req.ArtistName, req.SpotifyID, req.DeezerID, req.TidalID, req.QobuzID, req.ItemID)
availability, err := provider.CheckAvailabilityForItemID(req.ISRC, req.TrackName, req.ArtistName, req.SpotifyID, req.DeezerID, req.TidalID, req.QobuzID, req.DurationMS, req.ItemID)
if shouldAbortCancelledFallback(req.ItemID, err) {
return nil, ErrDownloadCancelled
}
+1
View File
@@ -406,6 +406,7 @@ func (r *extensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) {
"bitDepth": quality.BitDepth,
"sampleRate": quality.SampleRate,
"totalSamples": quality.TotalSamples,
"duration": quality.Duration,
})
})
+48 -1
View File
@@ -1592,6 +1592,7 @@ type AudioQuality struct {
BitDepth int `json:"bit_depth"`
SampleRate int `json:"sample_rate"`
TotalSamples int64 `json:"total_samples"`
Duration int `json:"duration"`
}
func GetAudioQuality(filePath string) (AudioQuality, error) {
@@ -1632,10 +1633,16 @@ func GetAudioQuality(filePath string) (AudioQuality, error) {
int64(streamInfo[16])<<8 |
int64(streamInfo[17])
duration := 0
if sampleRate > 0 && totalSamples > 0 {
duration = int(totalSamples / int64(sampleRate))
}
return AudioQuality{
BitDepth: bitsPerSample,
SampleRate: sampleRate,
TotalSamples: totalSamples,
Duration: duration,
}, nil
}
@@ -1676,6 +1683,7 @@ func GetM4AQuality(filePath string) (AudioQuality, error) {
moovStart := moovHeader.offset
moovEnd := moovHeader.offset + moovHeader.size
duration := readM4ADurationSeconds(f, moovHeader, fileSize)
sampleOffset, atomType, err := findAudioSampleEntry(f, moovStart, moovEnd, fileSize)
if err != nil {
@@ -1715,7 +1723,46 @@ func GetM4AQuality(filePath string) (AudioQuality, error) {
bitDepth = 16
}
return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil
return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate, Duration: duration}, nil
}
func readM4ADurationSeconds(f *os.File, moovHeader atomHeader, fileSize int64) int {
childStart := moovHeader.offset + moovHeader.headerSize
childSize := moovHeader.size - moovHeader.headerSize
mvhdHeader, found, err := findAtomInRange(f, childStart, childSize, "mvhd", fileSize)
if err != nil || !found {
return 0
}
payloadOffset := mvhdHeader.offset + mvhdHeader.headerSize
versionBuf := make([]byte, 1)
if _, err := f.ReadAt(versionBuf, payloadOffset); err != nil {
return 0
}
if versionBuf[0] == 1 {
buf := make([]byte, 32)
if _, err := f.ReadAt(buf, payloadOffset); err != nil {
return 0
}
timescale := binary.BigEndian.Uint32(buf[20:24])
duration := binary.BigEndian.Uint64(buf[24:32])
if timescale == 0 || duration == 0 {
return 0
}
return int(math.Round(float64(duration) / float64(timescale)))
}
buf := make([]byte, 20)
if _, err := f.ReadAt(buf, payloadOffset); err != nil {
return 0
}
timescale := binary.BigEndian.Uint32(buf[12:16])
duration := binary.BigEndian.Uint32(buf[16:20])
if timescale == 0 || duration == 0 {
return 0
}
return int(math.Round(float64(duration) / float64(timescale)))
}
func readALACSpecificConfig(f *os.File, sampleOffset, fileSize int64) (int, int, bool) {
+2 -2
View File
@@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart';
/// App version and info constants
/// Update version here only - all other files will reference this
class AppInfo {
static const String version = '4.3.1';
static const String buildNumber = '126';
static const String version = '4.5.0';
static const String buildNumber = '127';
static const String fullVersion = '$version+$buildNumber';
/// Shows "Internal" in debug builds, actual version in release.
+2 -2
View File
@@ -169,13 +169,13 @@ abstract class AppLocalizations {
/// Title shown on home when no providers are available yet
///
/// In en, this message translates to:
/// **'Home is empty'**
/// **'No search providers yet'**
String get homeEmptyTitle;
/// Subtitle shown on home when no providers are available yet
///
/// In en, this message translates to:
/// **'Install your first extension to unlock search and browsing.'**
/// **'Install an extension to continue.'**
String get homeEmptySubtitle;
/// Info text about supported URL types
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get homeSubtitle => 'Spotify-Link einfügen oder nach Namen suchen';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports =>
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get homeSubtitle => 'Paste a Spotify link or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get homeSubtitle => 'Coller un lien Spotify ou rechercher par nom';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Piste, Album, Playlist, Artiste URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsHi extends AppLocalizations {
String get homeSubtitle => 'Paste a Spotify link or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -3
View File
@@ -31,11 +31,10 @@ class AppLocalizationsId extends AppLocalizations {
'Tempel URL yang didukung atau cari berdasarkan nama';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'Belum ada provider pencarian';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Pasang ekstensi untuk melanjutkan.';
@override
String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get homeSubtitle => 'Spotify のリンクを貼り付けるか、名前で検索します';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'サポート: トラック、アルバム、プレイリスト、アーティスト、URL';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsKo extends AppLocalizations {
String get homeSubtitle => 'Spotify URL을 붙여 넣거나 검색';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => '지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get homeSubtitle => 'Paste a Spotify link or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get homeSubtitle => 'Paste a Spotify link or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get homeSubtitle => 'Вставьте ссылку Spotify или ищите по названию';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports =>
+2 -3
View File
@@ -31,11 +31,10 @@ class AppLocalizationsTr extends AppLocalizations {
'Bir Spotify bağlantısı yapıştırın veya şarkı arayın';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports =>
+2 -3
View File
@@ -30,11 +30,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get homeSubtitle => 'Paste a Spotify link or search by name';
@override
String get homeEmptyTitle => 'Home is empty';
String get homeEmptyTitle => 'No search providers yet';
@override
String get homeEmptySubtitle =>
'Install your first extension to unlock search and browsing.';
String get homeEmptySubtitle => 'Install an extension to continue.';
@override
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+2 -2
View File
@@ -29,11 +29,11 @@
"@homeSubtitle": {
"description": "Subtitle shown below search box"
},
"homeEmptyTitle": "Home is empty",
"homeEmptyTitle": "No search providers yet",
"@homeEmptyTitle": {
"description": "Title shown on home when no providers are available yet"
},
"homeEmptySubtitle": "Install your first extension to unlock search and browsing.",
"homeEmptySubtitle": "Install an extension to continue.",
"@homeEmptySubtitle": {
"description": "Subtitle shown on home when no providers are available yet"
},
+8
View File
@@ -29,6 +29,14 @@
"@homeSubtitle": {
"description": "Subtitle shown below search box"
},
"homeEmptyTitle": "Belum ada provider pencarian",
"@homeEmptyTitle": {
"description": "Title shown on home when no providers are available yet"
},
"homeEmptySubtitle": "Pasang ekstensi untuk melanjutkan.",
"@homeEmptySubtitle": {
"description": "Subtitle shown on home when no providers are available yet"
},
"homeSupports": "Mendukung: URL Track, Album, Playlist, Artis",
"@homeSupports": {
"description": "Info text about supported URL types"
+179 -57
View File
@@ -509,6 +509,27 @@ class _HomeTabState extends ConsumerState<HomeTab>
defaultBuiltInSearchProviderId;
}
bool _hasSearchProvider(
String? explicitSearchProvider,
List<Extension> extensions,
List<BuiltInProviderSpec> builtInProviders,
) {
final explicit = explicitSearchProvider?.trim();
if (explicit != null && explicit.isNotEmpty) {
if (builtInProviders.any((p) => p.supportsSearch && p.id == explicit)) {
return true;
}
if (extensions.any(
(ext) => ext.enabled && ext.hasCustomSearch && ext.id == explicit,
)) {
return true;
}
}
return extensions.any((ext) => ext.enabled && ext.hasCustomSearch) ||
builtInProviders.any((provider) => provider.supportsSearch);
}
String? _sanitizeSearchFilterForProvider(
String? filter,
String? currentSearchProvider,
@@ -707,6 +728,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
bool _isLiveSearchEnabled() {
final settings = ref.read(settingsProvider);
final extState = ref.read(extensionProvider);
if (!extState.isInitialized && extState.error == null) return true;
final searchProvider = _resolveSearchProvider(
settings.searchProvider,
extState.extensions,
@@ -774,8 +797,13 @@ class _HomeTabState extends ConsumerState<HomeTab>
}
Future<void> _performSearch(String query, {String? filterOverride}) async {
var extState = ref.read(extensionProvider);
if (!extState.isInitialized && extState.error == null) {
await ref.read(extensionProvider.notifier).waitForInitialization();
extState = ref.read(extensionProvider);
}
final settings = ref.read(settingsProvider);
final extState = ref.read(extensionProvider);
final searchProvider = _resolveSearchProvider(
settings.searchProvider,
extState.extensions,
@@ -1015,9 +1043,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
);
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)),
@@ -1299,6 +1325,15 @@ class _HomeTabState extends ConsumerState<HomeTab>
settingsProvider.select((s) => s.defaultSearchTab),
);
final extensions = ref.watch(extensionProvider.select((s) => s.extensions));
final extensionReadiness = ref.watch(
extensionProvider.select(
(s) => (
isInitialized: s.isInitialized,
error: s.error,
builtInProviders: s.builtInProviders,
),
),
);
final hasExploreContent = ref.watch(
exploreProvider.select((s) => s.sections.isNotEmpty),
@@ -1336,9 +1371,14 @@ class _HomeTabState extends ConsumerState<HomeTab>
recentModeRequested &&
(!hasSearchInput || hasShortSearchInput || !hasActualResults) &&
!isLoading;
final hasSearchProvider =
(_resolveSearchProvider(explicitSearchProvider, extensions) ?? '')
.isNotEmpty;
final isSearchProviderLoading =
!extensionReadiness.isInitialized && extensionReadiness.error == null;
final hasSearchProvider = _hasSearchProvider(
explicitSearchProvider,
extensions,
extensionReadiness.builtInProviders,
);
final showSearchBar = hasSearchProvider || isSearchProviderLoading;
final hasResults =
hasSearchInput || hasActualResults || isLoading || showRecentAccess;
final showExplore =
@@ -1349,6 +1389,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
(hasHomeFeedExtension || hasExploreContent) &&
hasExploreContent;
final showEmptyHomeState =
!isSearchProviderLoading &&
!hasSearchProvider &&
!hasHomeFeedExtension &&
!hasExploreContent &&
@@ -1442,56 +1483,15 @@ class _HomeTabState extends ConsumerState<HomeTab>
curve: Curves.easeOut,
child: (hasResults || showExplore)
? const SizedBox.shrink()
: Column(
children: [
SizedBox(height: screenHeight * 0.06),
Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
),
child: Image.asset(
'assets/images/logo-transparant.png',
color: colorScheme.onPrimary,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
width: 96,
height: 96,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(height: 16),
Text(
showEmptyHomeState
? context.l10n.homeEmptyTitle
: 'SpotiFLAC Mobile',
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
showEmptyHomeState
? context.l10n.homeEmptySubtitle
: context.l10n.homeSubtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
: _buildHomeIntro(
colorScheme: colorScheme,
screenHeight: screenHeight,
showEmptyHomeState: showEmptyHomeState,
),
),
),
if (hasSearchProvider)
if (showSearchBar)
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.fromLTRB(
@@ -1690,6 +1690,98 @@ class _HomeTabState extends ConsumerState<HomeTab>
);
}
Widget _buildHomeIntro({
required ColorScheme colorScheme,
required double screenHeight,
required bool showEmptyHomeState,
}) {
if (showEmptyHomeState) {
final emptyHeight = (screenHeight - 220).clamp(280.0, 520.0).toDouble();
return SizedBox(
height: emptyHeight,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.extension_outlined,
size: 56,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
context.l10n.homeEmptyTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
context.l10n.homeEmptySubtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: screenHeight * 0.06),
Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
),
child: Image.asset(
'assets/images/logo-transparant.png',
color: colorScheme.onPrimary,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
width: 96,
height: 96,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(height: 16),
Text(
'SpotiFLAC Mobile',
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
context.l10n.homeSubtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
void _onEmbeddedCoverChanged() {
if (!mounted || _embeddedCoverRefreshScheduled) return;
_embeddedCoverRefreshScheduled = true;
@@ -2176,9 +2268,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
);
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))),
);
@@ -3636,13 +3726,45 @@ class _SearchProviderDropdown extends ConsumerWidget {
final rawCurrentProvider = ref.watch(
settingsProvider.select((s) => s.searchProvider),
);
final extensions = ref.watch(extensionProvider.select((s) => s.extensions));
final extensionState = ref.watch(extensionProvider);
final extensions = extensionState.extensions;
final colorScheme = Theme.of(context).colorScheme;
final searchProviders = extensions
.where((ext) => ext.enabled && ext.hasCustomSearch)
.toList();
final builtInProviders = builtInSearchProviderSpecs;
final hasAnyProvider =
searchProviders.isNotEmpty || builtInProviders.isNotEmpty;
final isProviderLoading =
!extensionState.isInitialized && extensionState.error == null;
if (!hasAnyProvider) {
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: SizedBox(
width: 28,
height: 28,
child: Center(
child: isProviderLoading
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.primary,
),
)
: Icon(
Icons.search_off,
size: 20,
color: colorScheme.onSurfaceVariant,
),
),
),
);
}
final resolvedCurrentProvider =
rawCurrentProvider != null &&
rawCurrentProvider.isNotEmpty &&
+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer
publish_to: "none"
version: 4.3.1+126
version: 4.5.0+127
environment:
sdk: ^3.10.0