mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-12 17:37:55 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user