diff --git a/go_backend/exports.go b/go_backend/exports.go index edaa30dc..e593a9d6 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -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) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 53a05205..01072268 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -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 } diff --git a/go_backend/extension_runtime_utils.go b/go_backend/extension_runtime_utils.go index 2a3a9877..5bfb7250 100644 --- a/go_backend/extension_runtime_utils.go +++ b/go_backend/extension_runtime_utils.go @@ -406,6 +406,7 @@ func (r *extensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { "bitDepth": quality.BitDepth, "sampleRate": quality.SampleRate, "totalSamples": quality.TotalSamples, + "duration": quality.Duration, }) }) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 1fe52712..6ac35adf 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -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) { diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index ca485f66..a32ae917 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -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. diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 117a56a2..3cd4021e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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 diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e84b17f2..2fa23d94 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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 => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4969a1e8..0ccddc3b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 39c6229d..c8c330f4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8f51a8c5..1db6578e 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 041b487a..6ff73820 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -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'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index a1bdb8e5..3454dbdd 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4a4968fd..3e5c144f 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index bf02d357..4247c22f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f46029a7..ea67f5f7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 8843e67e..8e6baa5f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index da480b1f..3412bd6f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index d4b035f4..e3a57b79 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -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 => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 283a58c5..1e38d36a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7497bd67..d5ee9469 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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" }, diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 5f677922..830b74ad 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -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" diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 5b9002a2..d717972e 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -509,6 +509,27 @@ class _HomeTabState extends ConsumerState defaultBuiltInSearchProviderId; } + bool _hasSearchProvider( + String? explicitSearchProvider, + List extensions, + List 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 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 } Future _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 ); 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 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 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 (hasHomeFeedExtension || hasExploreContent) && hasExploreContent; final showEmptyHomeState = + !isSearchProviderLoading && !hasSearchProvider && !hasHomeFeedExtension && !hasExploreContent && @@ -1442,56 +1483,15 @@ class _HomeTabState extends ConsumerState 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 ); } + 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 ); 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 && diff --git a/pubspec.yaml b/pubspec.yaml index 8a2113b1..04a92b67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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