diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index d2a1027c..d021b40e 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -26,6 +26,8 @@ import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/home_feed_region_setting.dart'; import 'package:spotiflac_android/widgets/app_sliver_header.dart'; +part 'extensions_page_widgets.dart'; + class ExtensionsPage extends ConsumerStatefulWidget { const ExtensionsPage({super.key}); @@ -331,613 +333,3 @@ class _ExtensionsPageState extends ConsumerState { ); } } - -class _ExtensionItem extends StatelessWidget { - final Extension extension; - final ExtensionHealthStatus? healthStatus; - final bool showDivider; - final VoidCallback onTap; - final ValueChanged onToggle; - - const _ExtensionItem({ - required this.extension, - this.healthStatus, - required this.showDivider, - required this.onTap, - required this.onToggle, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final hasError = extension.status == 'error'; - final serviceHealthStatus = extension.hasServiceHealth - ? healthStatus?.status - : null; - final serviceHealthColor = serviceHealthStatus == null - ? null - : _extensionHealthColor(context, serviceHealthStatus); - - return ExtensionRow( - showDivider: showDivider, - onTap: onTap, - avatar: context.isMornye - ? null - : ExtensionAvatar( - filePath: extension.iconPath, - fallbackIcon: hasError ? Icons.error_outline : Icons.extension, - background: hasError - ? colorScheme.errorContainer - : colorScheme.primaryContainer, - foreground: hasError - ? colorScheme.error - : colorScheme.onPrimaryContainer, - ), - title: Text(extension.displayName), - subtitle: Text.rich( - hasError - ? TextSpan( - text: context.friendlyError( - extension.errorMessage, - fallback: context.l10n.extensionsErrorLoading, - ), - ) - : TextSpan( - text: 'v${extension.version}', - children: [ - if (serviceHealthStatus != null) ...[ - const TextSpan(text: ' · '), - TextSpan( - text: _extensionHealthLabel(context, serviceHealthStatus), - style: TextStyle(color: serviceHealthColor), - ), - ], - ], - ), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: hasError ? colorScheme.error : colorScheme.onSurfaceVariant, - ), - ), - trailing: AppSwitch( - value: extension.enabled, - semanticLabel: extension.displayName, - onChanged: hasError ? null : onToggle, - ), - ); - } -} - -Color _extensionHealthColor(BuildContext context, String status) { - final colorScheme = Theme.of(context).colorScheme; - switch (status) { - case 'online': - return context.isMornye - ? CupertinoColors.systemGreen.resolveFrom(context) - : colorScheme.primary; - case 'degraded': - return colorScheme.tertiary; - case 'offline': - return colorScheme.error; - default: - return colorScheme.onSurfaceVariant; - } -} - -String _extensionHealthLabel(BuildContext context, String status) { - switch (status) { - case 'online': - return context.l10n.extensionHealthOnline; - case 'degraded': - return context.l10n.extensionHealthDegraded; - case 'offline': - return context.l10n.extensionHealthOffline; - default: - return context.l10n.extensionHealthUnknown; - } -} - -class _ExtensionPriorityLink extends StatelessWidget { - const _ExtensionPriorityLink({ - required this.icon, - required this.title, - required this.subtitle, - required this.enabled, - required this.onTap, - }); - - final IconData icon; - final String title; - final String subtitle; - final bool enabled; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - if (context.isMornye) { - return Opacity( - opacity: enabled ? 1 : 0.5, - child: SettingsItem( - icon: icon, - title: title, - subtitle: subtitle, - onTap: enabled ? onTap : null, - ), - ); - } - - final content = InkWell( - onTap: enabled ? onTap : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - AppIcon( - icon, - color: enabled - ? colorScheme.onSurfaceVariant - : colorScheme.outline, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: enabled ? null : colorScheme.outline, - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - AppIcon( - Icons.chevron_right, - color: enabled - ? colorScheme.onSurfaceVariant - : colorScheme.outline, - ), - ], - ), - ), - ); - return SettingsSearchTarget(label: title, child: content); - } -} - -class _DownloadPriorityItem extends ConsumerWidget { - const _DownloadPriorityItem(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final extState = ref.watch(extensionProvider); - final hasDownloadExtensions = extState.extensions.any( - (e) => e.enabled && e.hasDownloadProvider, - ); - - return _ExtensionPriorityLink( - icon: Icons.download, - title: context.l10n.extensionsDownloadPriority, - subtitle: hasDownloadExtensions - ? context.l10n.extensionsDownloadPrioritySubtitle - : context.l10n.extensionsNoDownloadProvider, - enabled: hasDownloadExtensions, - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const ProviderPriorityPage()), - ), - ); - } -} - -class _MetadataPriorityItem extends ConsumerWidget { - const _MetadataPriorityItem(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final extState = ref.watch(extensionProvider); - - final hasMetadataExtensions = extState.extensions.any( - (e) => e.enabled && e.hasMetadataProvider, - ); - - return _ExtensionPriorityLink( - icon: Icons.search, - title: context.l10n.extensionsMetadataPriority, - subtitle: hasMetadataExtensions - ? context.l10n.extensionsMetadataPrioritySubtitle - : context.l10n.extensionsNoMetadataProvider, - enabled: hasMetadataExtensions, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const MetadataProviderPriorityPage(), - ), - ), - ); - } -} - -class _DownloadFallbackItem extends ConsumerWidget { - const _DownloadFallbackItem(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final extState = ref.watch(extensionProvider); - - final hasDownloadExtensions = extState.extensions.any( - (e) => e.enabled && e.hasDownloadProvider, - ); - - return _ExtensionPriorityLink( - icon: Icons.alt_route, - title: context.l10n.extensionsFallbackTitle, - subtitle: hasDownloadExtensions - ? context.l10n.extensionsFallbackSubtitle - : context.l10n.extensionsNoDownloadProvider, - enabled: hasDownloadExtensions, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const DownloadFallbackExtensionsPage(), - ), - ), - ); - } -} - -class _SearchProviderSelector extends ConsumerWidget { - const _SearchProviderSelector(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(settingsProvider); - final extState = ref.watch(extensionProvider); - final colorScheme = Theme.of(context).colorScheme; - - final searchProviders = extState.extensions - .where((e) => e.enabled && e.hasCustomSearch) - .toList(); - - final hasAnyProvider = searchProviders.isNotEmpty; - - final resolvedProviderId = - (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) - ? settings.searchProvider! - : searchProviders.firstOrNull?.id; - String currentProviderName = context.l10n.optionsPrimaryProviderSubtitle; - if (resolvedProviderId != null && resolvedProviderId.isNotEmpty) { - final ext = searchProviders - .where((e) => e.id == resolvedProviderId) - .firstOrNull; - currentProviderName = ext?.displayName ?? resolvedProviderId; - } - - if (context.isMornye) { - return Opacity( - opacity: hasAnyProvider ? 1 : 0.5, - child: SettingsItem( - icon: Icons.manage_search, - title: context.l10n.extensionsSearchProvider, - subtitle: hasAnyProvider - ? currentProviderName - : context.l10n.extensionsNoCustomSearch, - onTap: hasAnyProvider - ? () => _showSearchProviderPicker( - context, - ref, - settings, - searchProviders, - ) - : null, - ), - ); - } - - final content = Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - onTap: !hasAnyProvider - ? null - : () => _showSearchProviderPicker( - context, - ref, - settings, - searchProviders, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - AppIcon( - Icons.manage_search, - color: !hasAnyProvider - ? colorScheme.outline - : colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.extensionsSearchProvider, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: !hasAnyProvider ? colorScheme.outline : null, - ), - ), - const SizedBox(height: 2), - Text( - !hasAnyProvider - ? context.l10n.extensionsNoCustomSearch - : currentProviderName, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - AppIcon( - Icons.chevron_right, - color: !hasAnyProvider - ? colorScheme.outline - : colorScheme.onSurfaceVariant, - ), - ], - ), - ), - ), - ], - ); - return SettingsSearchTarget( - label: context.l10n.extensionsSearchProvider, - child: content, - ); - } - - void _showSearchProviderPicker( - BuildContext context, - WidgetRef ref, - AppSettings settings, - List searchProviders, - ) { - final colorScheme = Theme.of(context).colorScheme; - - showAppScrollableBottomSheet( - context: context, - title: context.l10n.extensionsSearchProvider, - subtitle: context.l10n.extensionsSearchProviderDescription, - builder: (ctx, controller) => SingleChildScrollView( - controller: controller, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...searchProviders.map( - (ext) => AppSheetOption( - leading: AppIcon(Icons.extension, color: colorScheme.secondary), - title: Text(ext.displayName), - subtitle: Text( - ext.searchBehavior?.placeholder ?? - ctx.l10n.extensionsCustomSearch, - ), - selected: settings.searchProvider == ext.id, - trailing: settings.searchProvider == ext.id - ? AppIcon(Icons.check_circle, color: colorScheme.primary) - : AppIcon( - Icons.circle_outlined, - color: colorScheme.outline, - ), - onTap: () { - ref.read(settingsProvider.notifier).setSearchProvider(ext.id); - Navigator.pop(ctx); - }, - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } -} - -class _HomeFeedProviderSelector extends ConsumerWidget { - const _HomeFeedProviderSelector(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(settingsProvider); - final extState = ref.watch(extensionProvider); - final colorScheme = Theme.of(context).colorScheme; - - final homeFeedProviders = extState.extensions - .where((e) => e.enabled && e.hasHomeFeed) - .toList(); - - final hasAnyProvider = homeFeedProviders.isNotEmpty; - final homeFeedDisabled = - settings.homeFeedProvider == AppSettings.homeFeedProviderOff; - - String currentProviderName = context.l10n.extensionsHomeFeedAuto; - if (homeFeedDisabled) { - currentProviderName = context.l10n.extensionsHomeFeedOff; - } else if (settings.homeFeedProvider != null && - settings.homeFeedProvider!.isNotEmpty) { - final ext = homeFeedProviders - .where((e) => e.id == settings.homeFeedProvider) - .firstOrNull; - currentProviderName = ext?.displayName ?? settings.homeFeedProvider!; - } - - if (context.isMornye) { - return SettingsItem( - icon: Icons.explore_outlined, - title: context.l10n.extensionsHomeFeedProvider, - subtitle: !hasAnyProvider && !homeFeedDisabled - ? context.l10n.extensionsNoHomeFeedExtensions - : currentProviderName, - showDivider: false, - onTap: () => _showHomeFeedProviderPicker( - context, - ref, - settings, - homeFeedProviders, - ), - ); - } - - final content = Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - onTap: () => _showHomeFeedProviderPicker( - context, - ref, - settings, - homeFeedProviders, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - AppIcon( - Icons.explore_outlined, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.extensionsHomeFeedProvider, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 2), - Text( - !hasAnyProvider && !homeFeedDisabled - ? context.l10n.extensionsNoHomeFeedExtensions - : currentProviderName, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - AppIcon( - Icons.chevron_right, - color: colorScheme.onSurfaceVariant, - ), - ], - ), - ), - ), - ], - ); - return SettingsSearchTarget( - label: context.l10n.extensionsHomeFeedProvider, - child: content, - ); - } - - void _showHomeFeedProviderPicker( - BuildContext context, - WidgetRef ref, - AppSettings settings, - List homeFeedProviders, - ) { - final colorScheme = Theme.of(context).colorScheme; - - showAppScrollableBottomSheet( - context: context, - title: context.l10n.extensionsHomeFeedProvider, - subtitle: context.l10n.extensionsHomeFeedDescription, - builder: (ctx, controller) => SingleChildScrollView( - controller: controller, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppSheetOption( - leading: AppIcon(Icons.auto_awesome, color: colorScheme.primary), - title: Text(ctx.l10n.extensionsHomeFeedAuto), - subtitle: Text(ctx.l10n.extensionsHomeFeedAutoSubtitle), - trailing: - (settings.homeFeedProvider == null || - settings.homeFeedProvider!.isEmpty) - ? AppIcon(Icons.check_circle, color: colorScheme.primary) - : AppIcon(Icons.circle_outlined, color: colorScheme.outline), - onTap: () { - ref.read(settingsProvider.notifier).setHomeFeedProvider(null); - ref.read(exploreProvider.notifier).refresh(); - Navigator.pop(ctx); - }, - ), - AppSheetOption( - leading: AppIcon(Icons.block, color: colorScheme.error), - title: Text(ctx.l10n.extensionsHomeFeedOff), - subtitle: Text(ctx.l10n.extensionsHomeFeedOffSubtitle), - selected: - settings.homeFeedProvider == AppSettings.homeFeedProviderOff, - trailing: - settings.homeFeedProvider == AppSettings.homeFeedProviderOff - ? AppIcon(Icons.check_circle, color: colorScheme.primary) - : AppIcon(Icons.circle_outlined, color: colorScheme.outline), - onTap: () { - ref - .read(settingsProvider.notifier) - .setHomeFeedProvider(AppSettings.homeFeedProviderOff); - ref.read(exploreProvider.notifier).clear(); - Navigator.pop(ctx); - }, - ), - if (homeFeedProviders.isEmpty) - AppSheetOption( - enabled: false, - leading: AppIcon( - Icons.extension_off, - color: colorScheme.outline, - ), - title: Text(ctx.l10n.extensionsNoHomeFeedExtensions), - ), - ...homeFeedProviders.map( - (ext) => AppSheetOption( - leading: AppIcon(Icons.extension, color: colorScheme.secondary), - title: Text(ext.displayName), - subtitle: Text(ctx.l10n.extensionsHomeFeedUse(ext.displayName)), - selected: settings.homeFeedProvider == ext.id, - trailing: settings.homeFeedProvider == ext.id - ? AppIcon(Icons.check_circle, color: colorScheme.primary) - : AppIcon( - Icons.circle_outlined, - color: colorScheme.outline, - ), - onTap: () { - ref - .read(settingsProvider.notifier) - .setHomeFeedProvider(ext.id); - ref.read(exploreProvider.notifier).refresh(); - Navigator.pop(ctx); - }, - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } -} diff --git a/lib/screens/settings/extensions_page_widgets.dart b/lib/screens/settings/extensions_page_widgets.dart new file mode 100644 index 00000000..157dc21f --- /dev/null +++ b/lib/screens/settings/extensions_page_widgets.dart @@ -0,0 +1,611 @@ +part of 'extensions_page.dart'; + +class _ExtensionItem extends StatelessWidget { + final Extension extension; + final ExtensionHealthStatus? healthStatus; + final bool showDivider; + final VoidCallback onTap; + final ValueChanged onToggle; + + const _ExtensionItem({ + required this.extension, + this.healthStatus, + required this.showDivider, + required this.onTap, + required this.onToggle, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final hasError = extension.status == 'error'; + final serviceHealthStatus = extension.hasServiceHealth + ? healthStatus?.status + : null; + final serviceHealthColor = serviceHealthStatus == null + ? null + : _extensionHealthColor(context, serviceHealthStatus); + + return ExtensionRow( + showDivider: showDivider, + onTap: onTap, + avatar: context.isMornye + ? null + : ExtensionAvatar( + filePath: extension.iconPath, + fallbackIcon: hasError ? Icons.error_outline : Icons.extension, + background: hasError + ? colorScheme.errorContainer + : colorScheme.primaryContainer, + foreground: hasError + ? colorScheme.error + : colorScheme.onPrimaryContainer, + ), + title: Text(extension.displayName), + subtitle: Text.rich( + hasError + ? TextSpan( + text: context.friendlyError( + extension.errorMessage, + fallback: context.l10n.extensionsErrorLoading, + ), + ) + : TextSpan( + text: 'v${extension.version}', + children: [ + if (serviceHealthStatus != null) ...[ + const TextSpan(text: ' · '), + TextSpan( + text: _extensionHealthLabel(context, serviceHealthStatus), + style: TextStyle(color: serviceHealthColor), + ), + ], + ], + ), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: hasError ? colorScheme.error : colorScheme.onSurfaceVariant, + ), + ), + trailing: AppSwitch( + value: extension.enabled, + semanticLabel: extension.displayName, + onChanged: hasError ? null : onToggle, + ), + ); + } +} + +Color _extensionHealthColor(BuildContext context, String status) { + final colorScheme = Theme.of(context).colorScheme; + switch (status) { + case 'online': + return context.isMornye + ? CupertinoColors.systemGreen.resolveFrom(context) + : colorScheme.primary; + case 'degraded': + return colorScheme.tertiary; + case 'offline': + return colorScheme.error; + default: + return colorScheme.onSurfaceVariant; + } +} + +String _extensionHealthLabel(BuildContext context, String status) { + switch (status) { + case 'online': + return context.l10n.extensionHealthOnline; + case 'degraded': + return context.l10n.extensionHealthDegraded; + case 'offline': + return context.l10n.extensionHealthOffline; + default: + return context.l10n.extensionHealthUnknown; + } +} + +class _ExtensionPriorityLink extends StatelessWidget { + const _ExtensionPriorityLink({ + required this.icon, + required this.title, + required this.subtitle, + required this.enabled, + required this.onTap, + }); + + final IconData icon; + final String title; + final String subtitle; + final bool enabled; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + if (context.isMornye) { + return Opacity( + opacity: enabled ? 1 : 0.5, + child: SettingsItem( + icon: icon, + title: title, + subtitle: subtitle, + onTap: enabled ? onTap : null, + ), + ); + } + + final content = InkWell( + onTap: enabled ? onTap : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + AppIcon( + icon, + color: enabled + ? colorScheme.onSurfaceVariant + : colorScheme.outline, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: enabled ? null : colorScheme.outline, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + AppIcon( + Icons.chevron_right, + color: enabled + ? colorScheme.onSurfaceVariant + : colorScheme.outline, + ), + ], + ), + ), + ); + return SettingsSearchTarget(label: title, child: content); + } +} + +class _DownloadPriorityItem extends ConsumerWidget { + const _DownloadPriorityItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final extState = ref.watch(extensionProvider); + final hasDownloadExtensions = extState.extensions.any( + (e) => e.enabled && e.hasDownloadProvider, + ); + + return _ExtensionPriorityLink( + icon: Icons.download, + title: context.l10n.extensionsDownloadPriority, + subtitle: hasDownloadExtensions + ? context.l10n.extensionsDownloadPrioritySubtitle + : context.l10n.extensionsNoDownloadProvider, + enabled: hasDownloadExtensions, + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ProviderPriorityPage()), + ), + ); + } +} + +class _MetadataPriorityItem extends ConsumerWidget { + const _MetadataPriorityItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final extState = ref.watch(extensionProvider); + + final hasMetadataExtensions = extState.extensions.any( + (e) => e.enabled && e.hasMetadataProvider, + ); + + return _ExtensionPriorityLink( + icon: Icons.search, + title: context.l10n.extensionsMetadataPriority, + subtitle: hasMetadataExtensions + ? context.l10n.extensionsMetadataPrioritySubtitle + : context.l10n.extensionsNoMetadataProvider, + enabled: hasMetadataExtensions, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const MetadataProviderPriorityPage(), + ), + ), + ); + } +} + +class _DownloadFallbackItem extends ConsumerWidget { + const _DownloadFallbackItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final extState = ref.watch(extensionProvider); + + final hasDownloadExtensions = extState.extensions.any( + (e) => e.enabled && e.hasDownloadProvider, + ); + + return _ExtensionPriorityLink( + icon: Icons.alt_route, + title: context.l10n.extensionsFallbackTitle, + subtitle: hasDownloadExtensions + ? context.l10n.extensionsFallbackSubtitle + : context.l10n.extensionsNoDownloadProvider, + enabled: hasDownloadExtensions, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const DownloadFallbackExtensionsPage(), + ), + ), + ); + } +} + +class _SearchProviderSelector extends ConsumerWidget { + const _SearchProviderSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final extState = ref.watch(extensionProvider); + final colorScheme = Theme.of(context).colorScheme; + + final searchProviders = extState.extensions + .where((e) => e.enabled && e.hasCustomSearch) + .toList(); + + final hasAnyProvider = searchProviders.isNotEmpty; + + final resolvedProviderId = + (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) + ? settings.searchProvider! + : searchProviders.firstOrNull?.id; + String currentProviderName = context.l10n.optionsPrimaryProviderSubtitle; + if (resolvedProviderId != null && resolvedProviderId.isNotEmpty) { + final ext = searchProviders + .where((e) => e.id == resolvedProviderId) + .firstOrNull; + currentProviderName = ext?.displayName ?? resolvedProviderId; + } + + if (context.isMornye) { + return Opacity( + opacity: hasAnyProvider ? 1 : 0.5, + child: SettingsItem( + icon: Icons.manage_search, + title: context.l10n.extensionsSearchProvider, + subtitle: hasAnyProvider + ? currentProviderName + : context.l10n.extensionsNoCustomSearch, + onTap: hasAnyProvider + ? () => _showSearchProviderPicker( + context, + ref, + settings, + searchProviders, + ) + : null, + ), + ); + } + + final content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: !hasAnyProvider + ? null + : () => _showSearchProviderPicker( + context, + ref, + settings, + searchProviders, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + AppIcon( + Icons.manage_search, + color: !hasAnyProvider + ? colorScheme.outline + : colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.extensionsSearchProvider, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: !hasAnyProvider ? colorScheme.outline : null, + ), + ), + const SizedBox(height: 2), + Text( + !hasAnyProvider + ? context.l10n.extensionsNoCustomSearch + : currentProviderName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + AppIcon( + Icons.chevron_right, + color: !hasAnyProvider + ? colorScheme.outline + : colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ], + ); + return SettingsSearchTarget( + label: context.l10n.extensionsSearchProvider, + child: content, + ); + } + + void _showSearchProviderPicker( + BuildContext context, + WidgetRef ref, + AppSettings settings, + List searchProviders, + ) { + final colorScheme = Theme.of(context).colorScheme; + + showAppScrollableBottomSheet( + context: context, + title: context.l10n.extensionsSearchProvider, + subtitle: context.l10n.extensionsSearchProviderDescription, + builder: (ctx, controller) => SingleChildScrollView( + controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...searchProviders.map( + (ext) => AppSheetOption( + leading: AppIcon(Icons.extension, color: colorScheme.secondary), + title: Text(ext.displayName), + subtitle: Text( + ext.searchBehavior?.placeholder ?? + ctx.l10n.extensionsCustomSearch, + ), + selected: settings.searchProvider == ext.id, + trailing: settings.searchProvider == ext.id + ? AppIcon(Icons.check_circle, color: colorScheme.primary) + : AppIcon( + Icons.circle_outlined, + color: colorScheme.outline, + ), + onTap: () { + ref.read(settingsProvider.notifier).setSearchProvider(ext.id); + Navigator.pop(ctx); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} + +class _HomeFeedProviderSelector extends ConsumerWidget { + const _HomeFeedProviderSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final extState = ref.watch(extensionProvider); + final colorScheme = Theme.of(context).colorScheme; + + final homeFeedProviders = extState.extensions + .where((e) => e.enabled && e.hasHomeFeed) + .toList(); + + final hasAnyProvider = homeFeedProviders.isNotEmpty; + final homeFeedDisabled = + settings.homeFeedProvider == AppSettings.homeFeedProviderOff; + + String currentProviderName = context.l10n.extensionsHomeFeedAuto; + if (homeFeedDisabled) { + currentProviderName = context.l10n.extensionsHomeFeedOff; + } else if (settings.homeFeedProvider != null && + settings.homeFeedProvider!.isNotEmpty) { + final ext = homeFeedProviders + .where((e) => e.id == settings.homeFeedProvider) + .firstOrNull; + currentProviderName = ext?.displayName ?? settings.homeFeedProvider!; + } + + if (context.isMornye) { + return SettingsItem( + icon: Icons.explore_outlined, + title: context.l10n.extensionsHomeFeedProvider, + subtitle: !hasAnyProvider && !homeFeedDisabled + ? context.l10n.extensionsNoHomeFeedExtensions + : currentProviderName, + showDivider: false, + onTap: () => _showHomeFeedProviderPicker( + context, + ref, + settings, + homeFeedProviders, + ), + ); + } + + final content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => _showHomeFeedProviderPicker( + context, + ref, + settings, + homeFeedProviders, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + AppIcon( + Icons.explore_outlined, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.extensionsHomeFeedProvider, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 2), + Text( + !hasAnyProvider && !homeFeedDisabled + ? context.l10n.extensionsNoHomeFeedExtensions + : currentProviderName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + AppIcon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ], + ); + return SettingsSearchTarget( + label: context.l10n.extensionsHomeFeedProvider, + child: content, + ); + } + + void _showHomeFeedProviderPicker( + BuildContext context, + WidgetRef ref, + AppSettings settings, + List homeFeedProviders, + ) { + final colorScheme = Theme.of(context).colorScheme; + + showAppScrollableBottomSheet( + context: context, + title: context.l10n.extensionsHomeFeedProvider, + subtitle: context.l10n.extensionsHomeFeedDescription, + builder: (ctx, controller) => SingleChildScrollView( + controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppSheetOption( + leading: AppIcon(Icons.auto_awesome, color: colorScheme.primary), + title: Text(ctx.l10n.extensionsHomeFeedAuto), + subtitle: Text(ctx.l10n.extensionsHomeFeedAutoSubtitle), + trailing: + (settings.homeFeedProvider == null || + settings.homeFeedProvider!.isEmpty) + ? AppIcon(Icons.check_circle, color: colorScheme.primary) + : AppIcon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref.read(settingsProvider.notifier).setHomeFeedProvider(null); + ref.read(exploreProvider.notifier).refresh(); + Navigator.pop(ctx); + }, + ), + AppSheetOption( + leading: AppIcon(Icons.block, color: colorScheme.error), + title: Text(ctx.l10n.extensionsHomeFeedOff), + subtitle: Text(ctx.l10n.extensionsHomeFeedOffSubtitle), + selected: + settings.homeFeedProvider == AppSettings.homeFeedProviderOff, + trailing: + settings.homeFeedProvider == AppSettings.homeFeedProviderOff + ? AppIcon(Icons.check_circle, color: colorScheme.primary) + : AppIcon(Icons.circle_outlined, color: colorScheme.outline), + onTap: () { + ref + .read(settingsProvider.notifier) + .setHomeFeedProvider(AppSettings.homeFeedProviderOff); + ref.read(exploreProvider.notifier).clear(); + Navigator.pop(ctx); + }, + ), + if (homeFeedProviders.isEmpty) + AppSheetOption( + enabled: false, + leading: AppIcon( + Icons.extension_off, + color: colorScheme.outline, + ), + title: Text(ctx.l10n.extensionsNoHomeFeedExtensions), + ), + ...homeFeedProviders.map( + (ext) => AppSheetOption( + leading: AppIcon(Icons.extension, color: colorScheme.secondary), + title: Text(ext.displayName), + subtitle: Text(ctx.l10n.extensionsHomeFeedUse(ext.displayName)), + selected: settings.homeFeedProvider == ext.id, + trailing: settings.homeFeedProvider == ext.id + ? AppIcon(Icons.check_circle, color: colorScheme.primary) + : AppIcon( + Icons.circle_outlined, + color: colorScheme.outline, + ), + onTap: () { + ref + .read(settingsProvider.notifier) + .setHomeFeedProvider(ext.id); + ref.read(exploreProvider.notifier).refresh(); + Navigator.pop(ctx); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +}