mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-16 05:29:15 +02:00
refactor: remove built-in Spotify API provider, use Deezer as sole default
- Remove all Spotify credential management (client ID/secret, secure storage) - Remove Spotify platform channel handlers from MainActivity - Remove exported Go functions: GetSpotifyMetadata, SearchSpotify, SearchSpotifyAll, GetSpotifyRelatedArtists, SetSpotifyAPICredentials - Simplify GetSpotifyMetadataWithDeezerFallback to SpotFetch-only path - Remove Spotify built-in fallback in ReEnrichFile search pipeline - Always return false from HasSpotifyCredentials; getCredentials always errors - Default metadataProviderPriority is now ['deezer'] only - Sanitize provider priority list to strip 'spotify' entries on load/save - Add migration v5 to clear saved Spotify credentials from existing installs - Remove Spotify source chip and credentials UI from options settings page - Remove metadataSource param from search() — always uses Deezer - spotify-web extension remains supported via the extension provider system
This commit is contained in:
@@ -1877,38 +1877,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getSpotifyMetadata" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getSpotifyMetadata(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"searchSpotify" -> {
|
||||
val query = call.argument<String>("query") ?: ""
|
||||
val limit = call.argument<Int>("limit") ?: 10
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.searchSpotify(query, limit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"searchSpotifyAll" -> {
|
||||
val query = call.argument<String>("query") ?: ""
|
||||
val trackLimit = call.argument<Int>("track_limit") ?: 15
|
||||
val artistLimit = call.argument<Int>("artist_limit") ?: 3
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.searchSpotifyAll(query, trackLimit.toLong(), artistLimit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getSpotifyRelatedArtists" -> {
|
||||
val artistId = call.argument<String>("artist_id") ?: ""
|
||||
val limit = call.argument<Int>("limit") ?: 12
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getSpotifyRelatedArtists(artistId, limit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkAvailability" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
@@ -2542,20 +2510,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
"isDownloadServiceRunning" -> {
|
||||
result.success(DownloadService.isServiceRunning())
|
||||
}
|
||||
"setSpotifyCredentials" -> {
|
||||
val clientId = call.argument<String>("client_id") ?: ""
|
||||
val clientSecret = call.argument<String>("client_secret") ?: ""
|
||||
withContext(Dispatchers.IO) {
|
||||
Gobackend.setSpotifyAPICredentials(clientId, clientSecret)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"hasSpotifyCredentials" -> {
|
||||
val hasCredentials = withContext(Dispatchers.IO) {
|
||||
Gobackend.checkSpotifyCredentials()
|
||||
}
|
||||
result.success(hasCredentials)
|
||||
}
|
||||
"preWarmTrackCache" -> {
|
||||
val tracksJson = call.argument<String>("tracks") ?: "[]"
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+3
-185
@@ -32,126 +32,6 @@ func ParseSpotifyURL(url string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SetSpotifyAPICredentials(clientID, clientSecret string) {
|
||||
SetSpotifyCredentials(clientID, clientSecret)
|
||||
}
|
||||
|
||||
func CheckSpotifyCredentials() bool {
|
||||
return HasSpotifyCredentials()
|
||||
}
|
||||
|
||||
func GetSpotifyMetadata(spotifyURL string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := NewSpotifyMetadataClient()
|
||||
if err != nil {
|
||||
if shouldTrySpotFetchFallback(err) {
|
||||
data, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
|
||||
if apiErr == nil {
|
||||
jsonBytes, marshalErr := json.Marshal(data)
|
||||
if marshalErr != nil {
|
||||
return "", marshalErr
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
data, err := client.GetFilteredData(ctx, spotifyURL, false, 0)
|
||||
if err != nil {
|
||||
if shouldTrySpotFetchFallback(err) {
|
||||
fallbackData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
|
||||
if apiErr == nil {
|
||||
jsonBytes, marshalErr := json.Marshal(fallbackData)
|
||||
if marshalErr != nil {
|
||||
return "", marshalErr
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SearchSpotify(query string, limit int) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := NewSpotifyMetadataClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
results, err := client.SearchTracks(ctx, query, limit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SearchSpotifyAll(query string, trackLimit, artistLimit int) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := NewSpotifyMetadataClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
results, err := client.SearchAll(ctx, query, trackLimit, artistLimit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func GetSpotifyRelatedArtists(artistID string, limit int) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := NewSpotifyMetadataClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
normalizedArtistID := strings.TrimSpace(strings.TrimPrefix(artistID, "spotify:"))
|
||||
if normalizedArtistID == "" {
|
||||
return "", fmt.Errorf("invalid Spotify artist ID")
|
||||
}
|
||||
|
||||
artists, err := client.GetRelatedArtists(ctx, normalizedArtistID, limit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"artists": artists,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func CheckAvailability(spotifyID, isrc string) (string, error) {
|
||||
client := NewSongLinkClient()
|
||||
availability, err := client.CheckTrackAvailability(spotifyID, isrc)
|
||||
@@ -1439,28 +1319,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var spotifyErr error
|
||||
|
||||
client, err := NewSpotifyMetadataClient()
|
||||
if err != nil {
|
||||
LogWarn("Spotify", "Credentials not configured, falling back to Deezer")
|
||||
spotifyErr = err
|
||||
} else {
|
||||
data, err := client.GetFilteredData(ctx, spotifyURL, false, 0)
|
||||
if err == nil {
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
spotifyErr = err
|
||||
if !shouldTrySpotFetchFallback(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
|
||||
if apiErr == nil {
|
||||
GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n")
|
||||
@@ -1474,9 +1332,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
|
||||
|
||||
parsed, parseErr := parseSpotifyURI(spotifyURL)
|
||||
if parseErr != nil {
|
||||
if spotifyErr != nil {
|
||||
return "", fmt.Errorf("spotify failed (%v), SpotFetch fallback failed (%v), and URL parsing failed: %w", spotifyErr, apiErr, parseErr)
|
||||
}
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr)
|
||||
}
|
||||
|
||||
@@ -1487,15 +1342,9 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
|
||||
}
|
||||
|
||||
if parsed.Type == "artist" {
|
||||
if spotifyErr != nil {
|
||||
return "", fmt.Errorf("spotify metadata unavailable (%v) and SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", spotifyErr, apiErr)
|
||||
}
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", apiErr)
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages now require SpotFetch or a metadata extension such as spotify-web", apiErr)
|
||||
}
|
||||
|
||||
if spotifyErr != nil {
|
||||
return "", fmt.Errorf("spotify metadata unavailable (%v), SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", spotifyErr, apiErr)
|
||||
}
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr)
|
||||
}
|
||||
|
||||
@@ -1826,8 +1675,8 @@ func ReEnrichFile(requestJSON string) (string, error) {
|
||||
|
||||
GoLog("[ReEnrich] Starting re-enrichment for: %s\n", req.FilePath)
|
||||
|
||||
// When search_online is true, search for metadata from internet
|
||||
// Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) 3) Spotify built-in API (last resort, deprecated)
|
||||
// When search_online is true, search for metadata from internet.
|
||||
// Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc)
|
||||
if req.SearchOnline && req.TrackName != "" && req.ArtistName != "" {
|
||||
GoLog("[ReEnrich] Searching online metadata for: %s - %s\n", req.TrackName, req.ArtistName)
|
||||
searchQuery := req.TrackName + " " + req.ArtistName
|
||||
@@ -1901,37 +1750,6 @@ func ReEnrichFile(requestJSON string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Try Spotify built-in API as last resort (will be deprecated)
|
||||
if !found {
|
||||
GoLog("[ReEnrich] Trying Spotify API (fallback)...\n")
|
||||
spotifyClient, spotifyErr := NewSpotifyMetadataClient()
|
||||
if spotifyErr == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
results, err := spotifyClient.SearchTracks(ctx, searchQuery, 5)
|
||||
cancel()
|
||||
if err == nil && len(results.Tracks) > 0 {
|
||||
track := results.Tracks[0]
|
||||
GoLog("[ReEnrich] Spotify match: %s - %s (album: %s)\n", track.Name, track.Artists, track.AlbumName)
|
||||
req.SpotifyID = track.SpotifyID
|
||||
req.AlbumName = track.AlbumName
|
||||
req.AlbumArtist = track.AlbumArtist
|
||||
req.TrackNumber = track.TrackNumber
|
||||
req.DiscNumber = track.DiscNumber
|
||||
req.ReleaseDate = track.ReleaseDate
|
||||
req.ISRC = track.ISRC
|
||||
if track.Images != "" {
|
||||
req.CoverURL = track.Images
|
||||
}
|
||||
req.DurationMs = int64(track.DurationMS)
|
||||
found = true
|
||||
} else if err != nil {
|
||||
GoLog("[ReEnrich] Spotify search failed: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
GoLog("[ReEnrich] Spotify client unavailable: %v\n", spotifyErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get extended metadata (genre, label) from Deezer if not already set
|
||||
if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
@@ -644,8 +644,26 @@ func GetProviderPriority() []string {
|
||||
func SetMetadataProviderPriority(providerIDs []string) {
|
||||
metadataProviderPriorityMu.Lock()
|
||||
defer metadataProviderPriorityMu.Unlock()
|
||||
metadataProviderPriority = providerIDs
|
||||
GoLog("[Extension] Metadata provider priority set: %v\n", providerIDs)
|
||||
|
||||
sanitized := make([]string, 0, len(providerIDs)+1)
|
||||
seen := map[string]struct{}{}
|
||||
for _, providerID := range providerIDs {
|
||||
providerID = strings.TrimSpace(providerID)
|
||||
if providerID == "" || providerID == "spotify" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[providerID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[providerID] = struct{}{}
|
||||
sanitized = append(sanitized, providerID)
|
||||
}
|
||||
if _, exists := seen["deezer"]; !exists {
|
||||
sanitized = append([]string{"deezer"}, sanitized...)
|
||||
}
|
||||
|
||||
metadataProviderPriority = sanitized
|
||||
GoLog("[Extension] Metadata provider priority set: %v\n", sanitized)
|
||||
}
|
||||
|
||||
func GetMetadataProviderPriority() []string {
|
||||
@@ -653,7 +671,7 @@ func GetMetadataProviderPriority() []string {
|
||||
defer metadataProviderPriorityMu.RUnlock()
|
||||
|
||||
if len(metadataProviderPriority) == 0 {
|
||||
return []string{"deezer", "spotify"}
|
||||
return []string{"deezer"}
|
||||
}
|
||||
|
||||
result := make([]string, len(metadataProviderPriority))
|
||||
|
||||
+3
-29
@@ -9,7 +9,6 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -64,45 +63,20 @@ var (
|
||||
credentialsMu sync.RWMutex
|
||||
)
|
||||
|
||||
var ErrNoSpotifyCredentials = errors.New("Spotify credentials not configured. Please set your own Client ID and Secret in Settings, or use Deezer as metadata source (free, no credentials required)")
|
||||
var ErrNoSpotifyCredentials = errors.New("built-in Spotify API metadata provider has been removed; use Deezer or the spotify-web extension instead")
|
||||
|
||||
func SetSpotifyCredentials(clientID, clientSecret string) {
|
||||
credentialsMu.Lock()
|
||||
defer credentialsMu.Unlock()
|
||||
customClientID = clientID
|
||||
customClientSecret = clientSecret
|
||||
customClientID = ""
|
||||
customClientSecret = ""
|
||||
}
|
||||
|
||||
func HasSpotifyCredentials() bool {
|
||||
credentialsMu.RLock()
|
||||
defer credentialsMu.RUnlock()
|
||||
|
||||
if customClientID != "" && customClientSecret != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if os.Getenv("SPOTIFY_CLIENT_ID") != "" && os.Getenv("SPOTIFY_CLIENT_SECRET") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getCredentials() (string, string, error) {
|
||||
credentialsMu.RLock()
|
||||
defer credentialsMu.RUnlock()
|
||||
|
||||
if customClientID != "" && customClientSecret != "" {
|
||||
return customClientID, customClientSecret, nil
|
||||
}
|
||||
|
||||
clientID := os.Getenv("SPOTIFY_CLIENT_ID")
|
||||
clientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET")
|
||||
|
||||
if clientID != "" && clientSecret != "" {
|
||||
return clientID, clientSecret, nil
|
||||
}
|
||||
|
||||
return "", "", ErrNoSpotifyCredentials
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class AppSettings {
|
||||
this.askQualityBeforeDownload = true,
|
||||
this.spotifyClientId = '',
|
||||
this.spotifyClientSecret = '',
|
||||
this.useCustomSpotifyCredentials = true,
|
||||
this.useCustomSpotifyCredentials = false,
|
||||
this.metadataSource = 'deezer',
|
||||
this.enableLogging = false,
|
||||
this.useExtensionProviders = true,
|
||||
@@ -149,7 +149,7 @@ class AppSettings {
|
||||
String? downloadDirectory,
|
||||
String? storageMode,
|
||||
String? downloadTreeUri,
|
||||
bool? autoFallback,
|
||||
bool? autoFallback,
|
||||
bool? embedMetadata,
|
||||
bool? embedLyrics,
|
||||
bool? maxQualityCover,
|
||||
|
||||
@@ -33,7 +33,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
spotifyClientId: json['spotifyClientId'] as String? ?? '',
|
||||
spotifyClientSecret: json['spotifyClientSecret'] as String? ?? '',
|
||||
useCustomSpotifyCredentials:
|
||||
json['useCustomSpotifyCredentials'] as bool? ?? true,
|
||||
json['useCustomSpotifyCredentials'] as bool? ?? false,
|
||||
metadataSource: json['metadataSource'] as String? ?? 'deezer',
|
||||
enableLogging: json['enableLogging'] as bool? ?? false,
|
||||
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
|
||||
|
||||
@@ -823,11 +823,19 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
List<String> priority;
|
||||
if (savedJson != null) {
|
||||
final saved = jsonDecode(savedJson) as List<dynamic>;
|
||||
priority = saved.map((e) => e as String).toList();
|
||||
priority = _sanitizeMetadataProviderPriority(
|
||||
saved.map((e) => e as String).toList(),
|
||||
);
|
||||
_log.d('Loaded metadata provider priority from prefs: $priority');
|
||||
await prefs.setString(
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(priority),
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
} else {
|
||||
priority = await PlatformBridge.getMetadataProviderPriority();
|
||||
priority = _sanitizeMetadataProviderPriority(
|
||||
await PlatformBridge.getMetadataProviderPriority(),
|
||||
);
|
||||
_log.d('Using default metadata provider priority: $priority');
|
||||
}
|
||||
|
||||
@@ -840,11 +848,15 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
Future<void> setMetadataProviderPriority(List<String> priority) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_metadataProviderPriorityKey, jsonEncode(priority));
|
||||
final sanitized = _sanitizeMetadataProviderPriority(priority);
|
||||
await prefs.setString(
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(sanitized),
|
||||
);
|
||||
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
state = state.copyWith(metadataProviderPriority: priority);
|
||||
_log.d('Saved metadata provider priority: $priority');
|
||||
await PlatformBridge.setMetadataProviderPriority(sanitized);
|
||||
state = state.copyWith(metadataProviderPriority: sanitized);
|
||||
_log.d('Saved metadata provider priority: $sanitized');
|
||||
} catch (e) {
|
||||
_log.e('Failed to set metadata provider priority: $e');
|
||||
state = state.copyWith(error: e.toString());
|
||||
@@ -880,7 +892,7 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
List<String> getAllMetadataProviders() {
|
||||
final providers = ['deezer', 'spotify'];
|
||||
final providers = ['deezer'];
|
||||
for (final ext in state.extensions) {
|
||||
if (ext.enabled && ext.hasMetadataProvider) {
|
||||
providers.add(ext.id);
|
||||
@@ -889,6 +901,23 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
return providers;
|
||||
}
|
||||
|
||||
List<String> _sanitizeMetadataProviderPriority(List<String> input) {
|
||||
final allowed = getAllMetadataProviders().toSet();
|
||||
final result = <String>[];
|
||||
|
||||
for (final provider in input) {
|
||||
if (allowed.contains(provider) && !result.contains(provider)) {
|
||||
result.add(provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.contains('deezer')) {
|
||||
result.insert(0, 'deezer');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Extension> get searchProviders {
|
||||
return state.extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
const _settingsKey = 'app_settings';
|
||||
const _migrationVersionKey = 'settings_migration_version';
|
||||
const _currentMigrationVersion = 4;
|
||||
const _currentMigrationVersion = 5;
|
||||
const _spotifyClientSecretKey = 'spotify_client_secret';
|
||||
final _log = AppLogger('SettingsProvider');
|
||||
|
||||
@@ -41,9 +41,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
await _normalizeSongLinkRegionIfNeeded();
|
||||
}
|
||||
|
||||
await _loadSpotifyClientSecret(prefs);
|
||||
|
||||
_applySpotifyCredentials();
|
||||
await _retireBuiltInSpotifyProvider();
|
||||
|
||||
LogBuffer.loggingEnabled = state.enableLogging;
|
||||
|
||||
@@ -105,6 +103,17 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
}
|
||||
state = state.copyWith(lyricsProviders: updatedProviders);
|
||||
}
|
||||
if (state.metadataSource != 'deezer' ||
|
||||
state.spotifyClientId.isNotEmpty ||
|
||||
state.spotifyClientSecret.isNotEmpty ||
|
||||
state.useCustomSpotifyCredentials) {
|
||||
state = state.copyWith(
|
||||
metadataSource: 'deezer',
|
||||
spotifyClientId: '',
|
||||
spotifyClientSecret: '',
|
||||
useCustomSpotifyCredentials: false,
|
||||
);
|
||||
}
|
||||
state = state.copyWith(lastSeenVersion: AppInfo.version);
|
||||
await prefs.setInt(_migrationVersionKey, _currentMigrationVersion);
|
||||
await _saveSettings();
|
||||
@@ -193,49 +202,28 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadSpotifyClientSecret(SharedPreferences prefs) async {
|
||||
Future<void> _retireBuiltInSpotifyProvider() async {
|
||||
final storedSecret = await _secureStorage.read(
|
||||
key: _spotifyClientSecretKey,
|
||||
);
|
||||
final prefsSecret = state.spotifyClientSecret;
|
||||
|
||||
if ((storedSecret == null || storedSecret.isEmpty) &&
|
||||
prefsSecret.isNotEmpty) {
|
||||
await _secureStorage.write(
|
||||
key: _spotifyClientSecretKey,
|
||||
value: prefsSecret,
|
||||
);
|
||||
}
|
||||
|
||||
final effectiveSecret = (storedSecret != null && storedSecret.isNotEmpty)
|
||||
? storedSecret
|
||||
: (prefsSecret.isNotEmpty ? prefsSecret : '');
|
||||
|
||||
if (effectiveSecret != state.spotifyClientSecret) {
|
||||
state = state.copyWith(spotifyClientSecret: effectiveSecret);
|
||||
}
|
||||
|
||||
if (prefsSecret.isNotEmpty) {
|
||||
await _saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _storeSpotifyClientSecret(String secret) async {
|
||||
if (secret.isEmpty) {
|
||||
if (storedSecret != null && storedSecret.isNotEmpty) {
|
||||
await _secureStorage.delete(key: _spotifyClientSecretKey);
|
||||
} else {
|
||||
await _secureStorage.write(key: _spotifyClientSecretKey, value: secret);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applySpotifyCredentials() async {
|
||||
if (state.spotifyClientId.isNotEmpty &&
|
||||
state.spotifyClientSecret.isNotEmpty) {
|
||||
await PlatformBridge.setSpotifyCredentials(
|
||||
state.spotifyClientId,
|
||||
state.spotifyClientSecret,
|
||||
);
|
||||
if (state.metadataSource == 'deezer' &&
|
||||
state.spotifyClientId.isEmpty &&
|
||||
state.spotifyClientSecret.isEmpty &&
|
||||
!state.useCustomSpotifyCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
metadataSource: 'deezer',
|
||||
spotifyClientId: '',
|
||||
spotifyClientSecret: '',
|
||||
useCustomSpotifyCredentials: false,
|
||||
);
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
void setDefaultService(String service) {
|
||||
@@ -396,45 +384,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setSpotifyClientId(String clientId) {
|
||||
state = state.copyWith(spotifyClientId: clientId);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
Future<void> setSpotifyClientSecret(String clientSecret) async {
|
||||
state = state.copyWith(spotifyClientSecret: clientSecret);
|
||||
await _storeSpotifyClientSecret(clientSecret);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
Future<void> setSpotifyCredentials(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
) async {
|
||||
state = state.copyWith(
|
||||
spotifyClientId: clientId,
|
||||
spotifyClientSecret: clientSecret,
|
||||
);
|
||||
await _storeSpotifyClientSecret(clientSecret);
|
||||
_saveSettings();
|
||||
_applySpotifyCredentials();
|
||||
}
|
||||
|
||||
Future<void> clearSpotifyCredentials() async {
|
||||
state = state.copyWith(spotifyClientId: '', spotifyClientSecret: '');
|
||||
await _storeSpotifyClientSecret('');
|
||||
_saveSettings();
|
||||
_applySpotifyCredentials();
|
||||
}
|
||||
|
||||
void setUseCustomSpotifyCredentials(bool enabled) {
|
||||
state = state.copyWith(useCustomSpotifyCredentials: enabled);
|
||||
_saveSettings();
|
||||
_applySpotifyCredentials();
|
||||
}
|
||||
|
||||
void setMetadataSource(String source) {
|
||||
state = state.copyWith(metadataSource: source);
|
||||
final normalized = source == 'deezer' ? 'deezer' : 'deezer';
|
||||
state = state.copyWith(metadataSource: normalized);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -458,7 +458,8 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
|
||||
// If URL doesn't match any known service, it's unrecognized
|
||||
final isSpotifyUrl = url.contains('open.spotify.com') ||
|
||||
final isSpotifyUrl =
|
||||
url.contains('open.spotify.com') ||
|
||||
url.contains('spotify.link') ||
|
||||
url.startsWith('spotify:');
|
||||
if (!isSpotifyUrl) {
|
||||
@@ -546,11 +547,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> search(
|
||||
String query, {
|
||||
String? metadataSource,
|
||||
String? filterOverride,
|
||||
}) async {
|
||||
Future<void> search(String query, {String? filterOverride}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
// Preserve selected filter during loading
|
||||
@@ -576,7 +573,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
searchProvider != null &&
|
||||
searchProvider.isNotEmpty;
|
||||
|
||||
final source = metadataSource ?? 'deezer';
|
||||
const source = 'deezer';
|
||||
|
||||
_log.i(
|
||||
'Search started: source=$source, query="$query", useExtensions=$useExtensions, filter=$currentFilter',
|
||||
@@ -602,32 +599,20 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Extension search failed, falling back to built-in: $e');
|
||||
_log.w('Extension search failed, falling back to Deezer: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (source == 'deezer') {
|
||||
_log.d('Calling Deezer search API...');
|
||||
results = await PlatformBridge.searchDeezerAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: currentFilter,
|
||||
);
|
||||
_log.i(
|
||||
'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums',
|
||||
);
|
||||
} else {
|
||||
_log.d('Calling Spotify search API...');
|
||||
results = await PlatformBridge.searchSpotifyAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
);
|
||||
_log.i(
|
||||
'Spotify returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists',
|
||||
);
|
||||
}
|
||||
_log.d('Calling Deezer search API...');
|
||||
results = await PlatformBridge.searchDeezerAll(
|
||||
query,
|
||||
trackLimit: 20,
|
||||
artistLimit: 2,
|
||||
filter: currentFilter,
|
||||
);
|
||||
_log.i(
|
||||
'Deezer returned ${(results['tracks'] as List?)?.length ?? 0} tracks, ${(results['artists'] as List?)?.length ?? 0} artists, ${(results['albums'] as List?)?.length ?? 0} albums',
|
||||
);
|
||||
|
||||
if (!_isRequestValid(requestId)) {
|
||||
_log.w('Search request cancelled (requestId=$requestId)');
|
||||
|
||||
+11
-23
@@ -549,11 +549,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
await ref
|
||||
.read(trackProvider.notifier)
|
||||
.search(
|
||||
query,
|
||||
metadataSource: settings.metadataSource,
|
||||
filterOverride: selectedFilter,
|
||||
);
|
||||
.search(query, filterOverride: selectedFilter);
|
||||
}
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
}
|
||||
@@ -587,26 +583,24 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
if (trackState.error != null && mounted) {
|
||||
final l10n = context.l10n;
|
||||
final errorMsg = trackState.error!;
|
||||
final isRateLimit = errorMsg.contains('429') ||
|
||||
final isRateLimit =
|
||||
errorMsg.contains('429') ||
|
||||
errorMsg.toLowerCase().contains('rate limit') ||
|
||||
errorMsg.toLowerCase().contains('too many requests');
|
||||
final displayMessage = errorMsg == 'url_not_recognized'
|
||||
? l10n.errorUrlNotRecognizedMessage
|
||||
: isRateLimit
|
||||
? l10n.errorRateLimitedMessage
|
||||
: l10n.errorUrlFetchFailed;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(displayMessage)),
|
||||
);
|
||||
? l10n.errorRateLimitedMessage
|
||||
: l10n.errorUrlFetchFailed;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(displayMessage)));
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
} else {
|
||||
_navigateToDetailIfNeeded();
|
||||
}
|
||||
} else {
|
||||
final settings = ref.read(settingsProvider);
|
||||
await ref
|
||||
.read(trackProvider.notifier)
|
||||
.search(url, metadataSource: settings.metadataSource);
|
||||
await ref.read(trackProvider.notifier).search(url);
|
||||
}
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
}
|
||||
@@ -2315,10 +2309,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.errorUrlNotRecognizedMessage,
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
style: TextStyle(color: colorScheme.error, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2971,9 +2962,6 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
final currentProvider = ref.watch(
|
||||
settingsProvider.select((s) => s.searchProvider),
|
||||
);
|
||||
final metadataSource = ref.watch(
|
||||
settingsProvider.select((s) => s.metadataSource),
|
||||
);
|
||||
final extensions = ref.watch(extensionProvider.select((s) => s.extensions));
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
@@ -3051,7 +3039,7 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
metadataSource == 'spotify' ? 'Spotify' : 'Deezer',
|
||||
'Deezer',
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
currentProvider == null || currentProvider.isEmpty
|
||||
|
||||
@@ -27,10 +27,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
_searchController = TextEditingController(text: widget.query);
|
||||
if (widget.query.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
ref
|
||||
.read(trackProvider.notifier)
|
||||
.search(widget.query, metadataSource: settings.metadataSource);
|
||||
ref.read(trackProvider.notifier).search(widget.query);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -44,10 +41,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
void _search() {
|
||||
final query = _searchController.text.trim();
|
||||
if (query.isNotEmpty) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
ref
|
||||
.read(trackProvider.notifier)
|
||||
.search(query, metadataSource: settings.metadataSource);
|
||||
ref.read(trackProvider.notifier).search(query);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,13 +228,6 @@ class _MetadataProviderItem extends StatelessWidget {
|
||||
description: context.l10n.metadataNoRateLimits,
|
||||
isBuiltIn: true,
|
||||
);
|
||||
case 'spotify':
|
||||
return _MetadataProviderInfo(
|
||||
name: 'Spotify',
|
||||
icon: Icons.music_note,
|
||||
description: context.l10n.metadataMayRateLimit,
|
||||
isBuiltIn: true,
|
||||
);
|
||||
default:
|
||||
return _MetadataProviderInfo(
|
||||
name: provider,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
@@ -73,68 +72,10 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_MetadataSourceSelector(
|
||||
currentSource: settings.metadataSource,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setMetadataSource(v),
|
||||
),
|
||||
if (settings.metadataSource == 'spotify') ...[
|
||||
if (settings.spotifyClientId.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.optionsSpotifyWarning,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onErrorContainer,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.key,
|
||||
title: context.l10n.optionsSpotifyCredentials,
|
||||
subtitle: settings.spotifyClientId.isNotEmpty
|
||||
? context.l10n.optionsSpotifyCredentialsConfigured(
|
||||
settings.spotifyClientId.length > 8
|
||||
? settings.spotifyClientId.substring(0, 8)
|
||||
: settings.spotifyClientId,
|
||||
)
|
||||
: context.l10n.optionsSpotifyCredentialsRequired,
|
||||
onTap: () =>
|
||||
_showSpotifyCredentialsDialog(context, ref, settings),
|
||||
trailing: Icon(
|
||||
settings.spotifyClientId.isNotEmpty
|
||||
? Icons.check_circle
|
||||
: Icons.error_outline,
|
||||
color: settings.spotifyClientId.isNotEmpty
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -372,214 +313,6 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSpotifyCredentialsDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppSettings settings,
|
||||
) {
|
||||
final clientIdController = TextEditingController(
|
||||
text: settings.spotifyClientId,
|
||||
);
|
||||
final clientSecretController = TextEditingController(
|
||||
text: settings.spotifyClientSecret,
|
||||
);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.l10n.credentialsTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.credentialsDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
TextField(
|
||||
controller: clientIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.credentialsClientId,
|
||||
hintText: context.l10n.credentialsClientIdHint,
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: clientSecretController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.credentialsClientSecret,
|
||||
hintText: context.l10n.credentialsClientSecretHint,
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final clientId = clientIdController.text.trim();
|
||||
final clientSecret = clientSecretController.text.trim();
|
||||
|
||||
if (clientId.isNotEmpty && clientSecret.isNotEmpty) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSpotifyCredentials(clientId, clientSecret);
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarCredentialsSaved,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarFillAllFields),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.actionSaveCredentials,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
if (settings.spotifyClientId.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.clearSpotifyCredentials();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarCredentialsCleared,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: Text(context.l10n.actionRemoveCredentials),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConcurrentDownloadsItem extends StatelessWidget {
|
||||
@@ -875,12 +608,8 @@ class _ChannelChip extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _MetadataSourceSelector extends ConsumerWidget {
|
||||
final String currentSource;
|
||||
final ValueChanged<String> onChanged;
|
||||
const _MetadataSourceSelector({
|
||||
required this.currentSource,
|
||||
required this.onChanged,
|
||||
});
|
||||
const _MetadataSourceSelector({required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -930,7 +659,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
_SourceChip(
|
||||
icon: Icons.graphic_eq,
|
||||
label: 'Deezer',
|
||||
isSelected: currentSource == 'deezer' && !hasExtensionSearch,
|
||||
isSelected: !hasExtensionSearch,
|
||||
onTap: () {
|
||||
if (hasExtensionSearch) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
@@ -938,18 +667,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
onChanged('deezer');
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_SourceChip(
|
||||
icon: Icons.music_note,
|
||||
label: 'Spotify',
|
||||
isSelected: currentSource == 'spotify' && !hasExtensionSearch,
|
||||
onTap: () {
|
||||
if (hasExtensionSearch) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
}
|
||||
onChanged('spotify');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasExtensionSearch) ...[
|
||||
@@ -964,7 +681,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.optionsSwitchBack,
|
||||
'Tap Deezer to switch back from extension',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -973,27 +690,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
if (currentSource == 'spotify' && !hasExtensionSearch) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 16,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.optionsSpotifyDeprecationWarning,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -20,51 +20,6 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getSpotifyMetadata(String url) async {
|
||||
_log.d('getSpotifyMetadata: $url');
|
||||
final result = await _channel.invokeMethod('getSpotifyMetadata', {
|
||||
'url': url,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchSpotify(
|
||||
String query, {
|
||||
int limit = 10,
|
||||
}) async {
|
||||
_log.d('searchSpotify: "$query" (limit: $limit)');
|
||||
final result = await _channel.invokeMethod('searchSpotify', {
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchSpotifyAll(
|
||||
String query, {
|
||||
int trackLimit = 15,
|
||||
int artistLimit = 3,
|
||||
}) async {
|
||||
_log.d('searchSpotifyAll: "$query"');
|
||||
final result = await _channel.invokeMethod('searchSpotifyAll', {
|
||||
'query': query,
|
||||
'track_limit': trackLimit,
|
||||
'artist_limit': artistLimit,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getSpotifyRelatedArtists(
|
||||
String artistId, {
|
||||
int limit = 12,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('getSpotifyRelatedArtists', {
|
||||
'artist_id': artistId,
|
||||
'limit': limit,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> checkAvailability(
|
||||
String spotifyId,
|
||||
String isrc,
|
||||
@@ -517,21 +472,6 @@ class PlatformBridge {
|
||||
return result as bool;
|
||||
}
|
||||
|
||||
static Future<void> setSpotifyCredentials(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
) async {
|
||||
await _channel.invokeMethod('setSpotifyCredentials', {
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
});
|
||||
}
|
||||
|
||||
static Future<bool> hasSpotifyCredentials() async {
|
||||
final result = await _channel.invokeMethod('hasSpotifyCredentials');
|
||||
return result as bool;
|
||||
}
|
||||
|
||||
static Future<void> preWarmTrackCache(
|
||||
List<Map<String, String>> tracks,
|
||||
) async {
|
||||
@@ -1313,5 +1253,4 @@ class PlatformBridge {
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user