refactor(settings): move extensions_page widgets into a part

The page file mixed the extensions_page State with standalone private
widgets and helpers. They move verbatim into extensions_page_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.
This commit is contained in:
zarzet committed 2026-10-18 23:49:17 +07:00
1 parent 6e43c03691
commit 0d2fef4c33
2 files changed
+613 -610

No files matched your search

+2 -610
View File
@@ -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<ExtensionsPage> {
);
}
}
class _ExtensionItem extends StatelessWidget {
final Extension extension;
final ExtensionHealthStatus? healthStatus;
final bool showDivider;
final VoidCallback onTap;
final ValueChanged<bool> 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<void>(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<void>(
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<void>(
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<Extension> searchProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showAppScrollableBottomSheet<void>(
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<Extension> homeFeedProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showAppScrollableBottomSheet<void>(
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),
],
),
),
);
}
}
@@ -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<bool> 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<void>(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<void>(
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<void>(
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<Extension> searchProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showAppScrollableBottomSheet<void>(
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<Extension> homeFeedProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showAppScrollableBottomSheet<void>(
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),
],
),
),
);
}
}