// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member part of 'extension_provider.dart'; /// Download/metadata/search provider priority: persistence, sanitizing, and /// reconciliation when extensions are installed, removed, or toggled. extension ExtensionNotifierProviderPriority on ExtensionNotifier { Future _reconcileDownloadProviderPriority() async { if (state.providerPriority.isEmpty) { return; } final sanitized = _sanitizeDownloadProviderPriority(state.providerPriority); if (_stringListEquals(sanitized, state.providerPriority)) { return; } final prefs = await SharedPreferences.getInstance(); await prefs.setString(_providerPriorityKey, jsonEncode(sanitized)); await PlatformBridge.setProviderPriority(sanitized); state = state.copyWith(providerPriority: sanitized); _log.d('Reconciled provider priority after extension update: $sanitized'); } Future _reconcileMetadataProviderPriority() async { if (state.metadataProviderPriority.isEmpty) { return; } final replaced = _replaceRetiredBuiltInMetadataProviders( state.metadataProviderPriority, ); final sanitized = _sanitizeMetadataProviderPriority(replaced); if (_stringListEquals(sanitized, state.metadataProviderPriority)) { return; } final prefs = await SharedPreferences.getInstance(); await prefs.setString(_metadataProviderPriorityKey, jsonEncode(sanitized)); await PlatformBridge.setMetadataProviderPriority(sanitized); state = state.copyWith(metadataProviderPriority: sanitized); _log.d( 'Reconciled metadata provider priority after extension update: $sanitized', ); } String? _firstEnabledExtensionDownloadProviderId() { return state.extensions .where((ext) => ext.enabled && ext.hasDownloadProvider) .map((ext) => ext.id) .firstOrNull; } String? _firstEnabledSearchProviderId() { return defaultSearchExtension(state.extensions)?.id; } String? _replacedBuiltInProviderFor( String providerId, bool Function(Extension ext) hasCapability, ) { final normalized = providerId.trim().toLowerCase(); if (normalized.isEmpty) return null; return state.extensions .where( (ext) => ext.enabled && hasCapability(ext) && ext.replacesBuiltInProviders.contains(normalized), ) .map((ext) => ext.id) .firstOrNull; } String? replacedBuiltInDownloadProviderFor(String providerId) => _replacedBuiltInProviderFor(providerId, (ext) => ext.hasDownloadProvider); String? replacedBuiltInSearchProviderFor(String providerId) => _replacedBuiltInProviderFor(providerId, (ext) => ext.hasCustomSearch); String? replacedBuiltInMetadataProviderFor(String providerId) => _replacedBuiltInProviderFor(providerId, (ext) => ext.hasMetadataProvider); bool downloadProviderReplacesLegacyProvider( String providerId, String legacyProviderId, ) { final normalizedProvider = providerId.trim().toLowerCase(); final normalizedLegacy = legacyProviderId.trim().toLowerCase(); if (normalizedProvider.isEmpty || normalizedLegacy.isEmpty) return false; if (normalizedProvider == normalizedLegacy) return true; final extension = state.extensions .where((ext) => ext.enabled && ext.hasDownloadProvider) .where((ext) => ext.id.toLowerCase() == normalizedProvider) .firstOrNull; return extension?.replacesBuiltInProviders.contains(normalizedLegacy) ?? false; } Future _reconcileDefaultDownloadService() async { final settings = ref.read(settingsProvider); final preferredExtensionId = _firstEnabledExtensionDownloadProviderId(); final currentService = settings.defaultService.trim(); if (currentService.isEmpty) { if (preferredExtensionId != null) { ref .read(settingsProvider.notifier) .setDefaultService(preferredExtensionId); _log.d( 'Adopted first enabled download extension as default service: $preferredExtensionId', ); } return; } final replacementExtensionId = replacedBuiltInDownloadProviderFor( currentService, ); if (replacementExtensionId != null) { ref .read(settingsProvider.notifier) .setDefaultService(replacementExtensionId); _log.d( 'Migrated retired built-in service $currentService to $replacementExtensionId', ); return; } final currentExtension = state.extensions .where((ext) => ext.id == currentService) .firstOrNull; final isMissingOrInvalidExtension = currentExtension == null || !currentExtension.enabled || !currentExtension.hasDownloadProvider; if (isMissingOrInvalidExtension) { final fallbackService = preferredExtensionId ?? ''; ref.read(settingsProvider.notifier).setDefaultService(fallbackService); _log.d( fallbackService.isEmpty ? 'Cleared default service because $currentService is no longer available' : 'Reset default service to $fallbackService because $currentService is no longer available', ); } } void _reconcileSearchProvider() { final settings = ref.read(settingsProvider); final currentSearchProvider = settings.searchProvider?.trim() ?? ''; final preferredSearchProvider = _firstEnabledSearchProviderId() ?? ''; if (currentSearchProvider.isEmpty) { if (preferredSearchProvider.isNotEmpty) { ref .read(settingsProvider.notifier) .setSearchProvider(preferredSearchProvider); _log.d( 'Adopted first enabled search provider as default: $preferredSearchProvider', ); } return; } final replacementExtensionId = replacedBuiltInSearchProviderFor( currentSearchProvider, ); if (replacementExtensionId != null) { ref .read(settingsProvider.notifier) .setSearchProvider(replacementExtensionId); _log.d( 'Migrated retired built-in search provider $currentSearchProvider to $replacementExtensionId', ); return; } final hasMatchingExtension = state.extensions.any( (ext) => ext.enabled && ext.hasCustomSearch && ext.id == currentSearchProvider, ); if (!hasMatchingExtension) { ref .read(settingsProvider.notifier) .setSearchProvider( preferredSearchProvider.isNotEmpty ? preferredSearchProvider : null, ); _log.d( preferredSearchProvider.isNotEmpty ? 'Reset stale search provider $currentSearchProvider to $preferredSearchProvider' : 'Cleared stale search provider because $currentSearchProvider is no longer available', ); } } Future> getExtensionSettings(String extensionId) async { try { return await PlatformBridge.getExtensionSettings(extensionId); } catch (e) { _log.e('Failed to get extension settings: $e'); return {}; } } Future setExtensionSettings( String extensionId, Map settings, ) async { try { await PlatformBridge.setExtensionSettings(extensionId, settings); _log.d('Updated settings for extension: $extensionId'); } catch (e) { _log.e('Failed to set extension settings: $e'); state = state.copyWith(error: e.toString()); } } /// Shared load path for the download/metadata priority lists: prefs first /// (sanitized), falling back to backend defaults, then persist + push the /// result back to the backend. Future> _loadPriorityList({ required String prefsKey, required String label, required List Function(List) sanitizeStored, required List Function(List) sanitizeBackend, required Future> Function() fetchBackend, required Future Function(List) pushBackend, }) async { final prefs = await SharedPreferences.getInstance(); final savedJson = prefs.getString(prefsKey); List priority; if (savedJson != null) { final saved = _tryDecodeStringListPreference(savedJson, prefsKey); if (saved != null) { priority = sanitizeStored(saved); _log.d('Loaded $label from prefs: $priority'); } else { await prefs.remove(prefsKey); priority = sanitizeBackend(await fetchBackend()); _log.d('Recovered $label from defaults: $priority'); } } else { priority = sanitizeBackend(await fetchBackend()); _log.d('Using default $label: $priority'); } await prefs.setString(prefsKey, jsonEncode(priority)); await pushBackend(priority); return priority; } Future loadProviderPriority() async { try { final priority = await _loadPriorityList( prefsKey: _providerPriorityKey, label: 'provider priority', sanitizeStored: _sanitizeDownloadProviderPriority, sanitizeBackend: _sanitizeDownloadProviderPriority, fetchBackend: PlatformBridge.getProviderPriority, pushBackend: PlatformBridge.setProviderPriority, ); state = state.copyWith(providerPriority: priority); } catch (e) { _log.e('Failed to load provider priority: $e'); } } Future setProviderPriority(List priority) async { try { final sanitized = _sanitizeDownloadProviderPriority(priority); final prefs = await SharedPreferences.getInstance(); await prefs.setString(_providerPriorityKey, jsonEncode(sanitized)); await PlatformBridge.setProviderPriority(sanitized); state = state.copyWith(providerPriority: sanitized); _log.d('Saved provider priority: $sanitized'); } catch (e) { _log.e('Failed to set provider priority: $e'); state = state.copyWith(error: e.toString()); } } List _sanitizeDownloadProviderPriority(List input) { final allowed = getAllDownloadProviders().toSet(); final preferredOrder = getAllDownloadProviders(); final result = []; for (final provider in input) { if (allowed.contains(provider) && !result.contains(provider)) { result.add(provider); } } for (final provider in preferredOrder) { if (!result.contains(provider)) { result.add(provider); } } return result; } Future loadMetadataProviderPriority() async { try { final priority = await _loadPriorityList( prefsKey: _metadataProviderPriorityKey, label: 'metadata provider priority', sanitizeStored: (saved) => _sanitizeMetadataProviderPriority( _replaceRetiredBuiltInMetadataProviders(saved), ), sanitizeBackend: _sanitizeMetadataProviderPriority, fetchBackend: PlatformBridge.getMetadataProviderPriority, pushBackend: PlatformBridge.setMetadataProviderPriority, ); state = state.copyWith(metadataProviderPriority: priority); } catch (e) { _log.e('Failed to load metadata provider priority: $e'); } } Future setMetadataProviderPriority(List priority) async { try { final prefs = await SharedPreferences.getInstance(); final sanitized = _sanitizeMetadataProviderPriority( _replaceRetiredBuiltInMetadataProviders(priority), ); await prefs.setString( _metadataProviderPriorityKey, jsonEncode(sanitized), ); 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()); } } Future cleanup() async { if (_cleanupInFlight) return; _cleanupInFlight = true; await _cleanupExtensions(reason: 'manual'); } List getAllDownloadProviders() { return _distinctProviderIds( state.extensions .where((ext) => ext.enabled && ext.hasDownloadProvider) .map((ext) => ext.id), ); } List getAllMetadataProviders() { final metadataExtensions = state.extensions .where((ext) => ext.enabled && ext.hasMetadataProvider) .toList(); final primarySearchMetadataExtensions = metadataExtensions .where((ext) => ext.searchBehavior?.primary == true) .map((ext) => ext.id); final otherMetadataExtensions = metadataExtensions .where((ext) => ext.searchBehavior?.primary != true) .map((ext) => ext.id); return _distinctProviderIds([ ...primarySearchMetadataExtensions, ...otherMetadataExtensions, ]); } List _distinctProviderIds(Iterable ids) { final seen = {}; final result = []; for (final id in ids) { final normalized = id.trim(); if (normalized.isNotEmpty && seen.add(normalized)) { result.add(normalized); } } return result; } List _replaceRetiredBuiltInMetadataProviders(List input) { final result = []; for (final provider in input) { final replacement = replacedBuiltInMetadataProviderFor(provider); final resolved = replacement ?? provider; if (!result.contains(resolved)) { result.add(resolved); } } return result; } List _sanitizeMetadataProviderPriority(List input) { final allowed = getAllMetadataProviders().toSet(); final preferredOrder = getAllMetadataProviders(); final result = []; for (final provider in input) { if (allowed.contains(provider) && !result.contains(provider)) { result.add(provider); } } if (result.isEmpty && preferredOrder.isNotEmpty) { return List.from(preferredOrder); } for (final provider in preferredOrder) { if (!result.contains(provider)) { result.add(provider); } } return result; } }