feat: remove default Spotify credentials, require user's own API key

- Remove hardcoded Spotify client ID/secret from Go backend
- Spotify now requires user to provide their own credentials
- Deezer remains free (no credentials required)
- Update UI to show 'Free' badge for Deezer, 'API Key' for Spotify
- Show warning card when Spotify selected without credentials
- Add hasSpotifyCredentials check to platform bridge
This commit is contained in:
zarzet
2026-01-12 02:10:40 +07:00
parent 26d84041c7
commit 2b9357cb6d
8 changed files with 207 additions and 119 deletions
+4 -6
View File
@@ -60,18 +60,16 @@ class SettingsNotifier extends Notifier<AppSettings> {
/// Apply current Spotify credentials to Go backend
Future<void> _applySpotifyCredentials() async {
// Only apply custom credentials if enabled and both fields are set
if (state.useCustomSpotifyCredentials &&
state.spotifyClientId.isNotEmpty &&
// Only apply if both fields are set
if (state.spotifyClientId.isNotEmpty &&
state.spotifyClientSecret.isNotEmpty) {
await PlatformBridge.setSpotifyCredentials(
state.spotifyClientId,
state.spotifyClientSecret,
);
} else {
// Clear to use default
await PlatformBridge.setSpotifyCredentials('', '');
}
// Note: If credentials are empty, Spotify API will return error
// User should use Deezer as metadata source instead
}
void setDefaultService(String service) {
+61 -23
View File
@@ -76,38 +76,50 @@ class OptionsSettingsPage extends ConsumerWidget {
.setMetadataSource(v),
),
if (settings.metadataSource == 'spotify') ...[
SettingsSwitchItem(
icon: Icons.toggle_on,
title: 'Use Custom Credentials',
subtitle: settings.useCustomSpotifyCredentials
? 'Using your credentials'
: 'Using default credentials',
value: settings.useCustomSpotifyCredentials,
onChanged: (v) {
ref
.read(settingsProvider.notifier)
.setUseCustomSpotifyCredentials(v);
if (v && settings.spotifyClientId.isEmpty) {
_showSpotifyCredentialsDialog(context, ref, settings);
}
},
showDivider: true,
),
// Info card about Spotify credentials requirement
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(
'Spotify requires your own API credentials. Get them free from developer.spotify.com',
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
fontSize: 12,
),
),
),
],
),
),
),
),
SettingsItem(
icon: Icons.key,
title: 'Set Credentials',
title: 'Spotify Credentials',
subtitle: settings.spotifyClientId.isNotEmpty
? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}'
: 'Not configured',
: 'Required - tap to configure',
onTap: () =>
_showSpotifyCredentialsDialog(context, ref, settings),
trailing: Icon(
settings.spotifyClientId.isNotEmpty
? Icons.edit
: Icons.add,
? Icons.check_circle
: Icons.error_outline,
color: settings.spotifyClientId.isNotEmpty
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.primary,
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.error,
size: 20,
),
showDivider: false,
@@ -820,6 +832,8 @@ class _MetadataSourceSelector extends ConsumerWidget {
_SourceChip(
icon: Icons.graphic_eq,
label: 'Deezer',
badge: 'Free',
badgeColor: colorScheme.tertiary,
// Not selected if extension is active
isSelected: currentSource == 'deezer' && !hasExtensionSearch,
onTap: () {
@@ -834,6 +848,8 @@ class _MetadataSourceSelector extends ConsumerWidget {
_SourceChip(
icon: Icons.music_note,
label: 'Spotify',
badge: 'API Key',
badgeColor: colorScheme.secondary,
// Not selected if extension is active
isSelected: currentSource == 'spotify' && !hasExtensionSearch,
onTap: () {
@@ -878,12 +894,16 @@ class _SourceChip extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback? onTap;
final String? badge;
final Color? badgeColor;
const _SourceChip({
required this.icon,
required this.label,
required this.isSelected,
this.onTap,
this.badge,
this.badgeColor,
});
@override
@@ -929,6 +949,24 @@ class _SourceChip extends StatelessWidget {
: colorScheme.onSurfaceVariant,
),
),
if (badge != null) ...[
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: (badgeColor ?? colorScheme.tertiary).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
badge!,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w500,
color: badgeColor ?? colorScheme.tertiary,
),
),
),
],
],
),
),
+2 -3
View File
@@ -380,11 +380,10 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
_clientIdController.text.trim(),
_clientSecretController.text.trim(),
);
ref.read(settingsProvider.notifier).setUseCustomSpotifyCredentials(true);
// Set search source to Spotify when using custom credentials
// Set search source to Spotify when credentials are provided
ref.read(settingsProvider.notifier).setMetadataSource('spotify');
} else {
// Use Deezer as default search source
// Use Deezer as default search source (free, no credentials required)
ref.read(settingsProvider.notifier).setMetadataSource('deezer');
}
+7 -1
View File
@@ -331,7 +331,6 @@ class PlatformBridge {
}
/// Set custom Spotify API credentials
/// Pass empty strings to use default credentials
static Future<void> setSpotifyCredentials(String clientId, String clientSecret) async {
await _channel.invokeMethod('setSpotifyCredentials', {
'client_id': clientId,
@@ -339,6 +338,13 @@ class PlatformBridge {
});
}
/// Check if Spotify credentials are configured
/// Returns true if credentials are available (custom or env vars)
static Future<bool> hasSpotifyCredentials() async {
final result = await _channel.invokeMethod('hasSpotifyCredentials');
return result as bool;
}
/// Pre-warm track ID cache for album/playlist tracks
/// This runs in background and returns immediately
/// Speeds up subsequent downloads by caching ISRC → Track ID mappings