mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-06 12:48:03 +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:
@@ -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