refactor: remove built-in provider registry in favor of extensions

All search, metadata, and download providers are now exclusively
supplied by extensions. The built-in provider registry that previously
exposed Deezer/Tidal/Qobuz as hardcoded providers is fully removed.

Removed across Go, Dart, Kotlin, and Swift:
- BuiltInProviderSpec class, registry, and all accessor helpers
- SearchProviderAllJSON, GetBuiltInProvidersJSON, ParseProviderURLJSON,
  ParseDeezerURLExport Go exports and their platform channel bindings
- Built-in provider items in search dropdown, service picker, and
  provider priority UI lists
- provider_ui_utils.dart helper file

Deezer metadata enrichment (ISRC lookup, extended metadata, cover
upgrade) remains fully functional through direct DeezerClient calls
in the download pipeline — these are not part of the provider
registry and are unaffected.

Mark deezer as a retired built-in metadata provider so stale user
priority lists are cleaned up on next launch.
This commit is contained in:
zarzet
2026-05-05 03:55:24 +07:00
parent cfc8e699f3
commit 101ab3f521
49 changed files with 275 additions and 1502 deletions
+14 -154
View File
@@ -5,67 +5,6 @@ import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
class BuiltInService {
final String id;
final String label;
final List<QualityOption> qualityOptions;
final bool isDisabled; // If true, service is grayed out (fallback only)
final String? disabledReason;
const BuiltInService({
required this.id,
required this.label,
required this.qualityOptions,
this.isDisabled = false,
this.disabledReason,
});
}
const _builtInServiceCatalog = {
'tidal': BuiltInService(
id: 'tidal',
label: 'Tidal',
qualityOptions: [
QualityOption(
id: 'LOSSLESS',
label: 'FLAC Lossless',
description: '16-bit / 44.1kHz',
),
QualityOption(
id: 'HI_RES',
label: 'Hi-Res FLAC',
description: '24-bit / up to 96kHz',
),
QualityOption(
id: 'HI_RES_LOSSLESS',
label: 'Hi-Res FLAC Max',
description: '24-bit / up to 192kHz',
),
],
),
'qobuz': BuiltInService(
id: 'qobuz',
label: 'Qobuz',
qualityOptions: [
QualityOption(
id: 'LOSSLESS',
label: 'FLAC Lossless',
description: '16-bit / 44.1kHz',
),
QualityOption(
id: 'HI_RES',
label: 'Hi-Res FLAC',
description: '24-bit / up to 96kHz',
),
QualityOption(
id: 'HI_RES_LOSSLESS',
label: 'Hi-Res FLAC Max',
description: '24-bit / up to 192kHz',
),
],
),
};
class DownloadServicePicker extends ConsumerStatefulWidget {
final String? trackName;
final String? artistName;
@@ -118,18 +57,6 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
late String _selectedService;
List<BuiltInService> _availableBuiltInServices() {
final availableIds = builtInDownloadProviderIds.toSet();
final services = <BuiltInService>[];
for (final id in availableIds) {
final service = _builtInServiceCatalog[id];
if (service != null) {
services.add(service);
}
}
return services;
}
List<Extension> _downloadExtensions() {
final extensionState = ref.read(extensionProvider);
return extensionState.extensions
@@ -137,13 +64,8 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
.toList(growable: false);
}
bool _serviceExists(
String serviceId,
List<BuiltInService> builtInServices,
List<Extension> downloadExtensions,
) {
bool _serviceExists(String serviceId, List<Extension> downloadExtensions) {
if (serviceId.isEmpty) return false;
if (builtInServices.any((service) => service.id == serviceId)) return true;
return downloadExtensions.any((ext) => ext.id == serviceId);
}
@@ -154,39 +76,21 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
if (!mounted) return;
ref.read(extensionProvider.notifier).refreshEnabledExtensionHealth();
});
final builtInServices = _availableBuiltInServices();
final downloadExtensions = _downloadExtensions();
final recommended = widget.recommendedService;
if (recommended != null &&
_serviceExists(recommended, builtInServices, downloadExtensions)) {
if (recommended != null && _serviceExists(recommended, downloadExtensions)) {
_selectedService = recommended;
} else {
_selectedService = ref.read(settingsProvider).defaultService;
}
if (!_serviceExists(
_selectedService,
builtInServices,
downloadExtensions,
)) {
_selectedService = builtInServices.isNotEmpty
? builtInServices.first.id
: downloadExtensions.isNotEmpty
if (!_serviceExists(_selectedService, downloadExtensions)) {
_selectedService = downloadExtensions.isNotEmpty
? downloadExtensions.first.id
: '';
}
}
List<QualityOption> _getQualityOptions(
List<BuiltInService> builtInServices,
List<Extension> downloadExtensions,
) {
final builtIn = builtInServices
.where((service) => service.id == _selectedService)
.firstOrNull;
if (builtIn != null) {
return builtIn.qualityOptions;
}
List<QualityOption> _getQualityOptions(List<Extension> downloadExtensions) {
final ext = downloadExtensions
.where((e) => e.id == _selectedService)
.firstOrNull;
@@ -201,14 +105,9 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final extensionState = ref.watch(extensionProvider);
final builtInServices = _availableBuiltInServices();
final downloadExtensions = _downloadExtensions();
final hasProviders =
builtInServices.isNotEmpty || downloadExtensions.isNotEmpty;
final qualityOptions = _getQualityOptions(
builtInServices,
downloadExtensions,
);
final hasProviders = downloadExtensions.isNotEmpty;
final qualityOptions = _getQualityOptions(downloadExtensions);
return SafeArea(
child: SingleChildScrollView(
@@ -257,21 +156,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
spacing: 8,
runSpacing: 8,
children: [
for (final service in builtInServices)
_ServiceChip(
label: service.isDisabled
? '${service.label} (${service.disabledReason})'
: widget.recommendedService == service.id
? '${service.label} (Recommended)'
: service.label,
isSelected: _selectedService == service.id,
isDisabled: service.isDisabled,
onTap: service.isDisabled
? null
: () => setState(
() => _selectedService = service.id,
),
),
for (final ext in downloadExtensions)
_ServiceChip(
label: widget.recommendedService == ext.id
@@ -302,19 +186,6 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
),
),
),
if (builtInServices.any(
(service) => service.id == _selectedService,
))
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
child: Text(
context.l10n.qualityNote,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
),
for (final quality in qualityOptions)
_QualityOption(
title: _localizedQualityLabel(context, quality),
@@ -429,7 +300,6 @@ class _ServiceChip extends StatelessWidget {
final VoidCallback? onTap;
final String? iconPath;
final String? healthStatus;
final bool isDisabled;
const _ServiceChip({
required this.label,
@@ -437,21 +307,18 @@ class _ServiceChip extends StatelessWidget {
required this.onTap,
this.iconPath,
this.healthStatus,
this.isDisabled = false,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: isDisabled ? null : onTap,
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: isDisabled
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
: isSelected
color: isSelected
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
@@ -465,7 +332,7 @@ class _ServiceChip extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (healthStatus != null) ...[
_ServiceHealthDot(status: healthStatus!, isDisabled: isDisabled),
_ServiceHealthDot(status: healthStatus!),
const SizedBox(width: 8),
],
if (iconPath != null) ...[
@@ -479,9 +346,7 @@ class _ServiceChip extends StatelessWidget {
errorBuilder: (context, error, stackTrace) => Icon(
Icons.extension,
size: 18,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.4)
: isSelected
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
@@ -493,9 +358,7 @@ class _ServiceChip extends StatelessWidget {
label,
style: TextStyle(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.4)
: isSelected
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
@@ -509,15 +372,12 @@ class _ServiceChip extends StatelessWidget {
class _ServiceHealthDot extends StatelessWidget {
final String status;
final bool isDisabled;
const _ServiceHealthDot({required this.status, required this.isDisabled});
const _ServiceHealthDot({required this.status});
@override
Widget build(BuildContext context) {
final color = isDisabled
? Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.3)
: _serviceHealthColor(status);
final color = _serviceHealthColor(status);
return Tooltip(
message: _serviceHealthTooltip(status),
child: Container(