diff --git a/app/lib/features/settings/presentation/screens/developer_settings.dart b/app/lib/features/settings/presentation/screens/developer_settings.dart index 14d3ea55..bead7023 100644 --- a/app/lib/features/settings/presentation/screens/developer_settings.dart +++ b/app/lib/features/settings/presentation/screens/developer_settings.dart @@ -38,18 +38,11 @@ import 'package:weblibre/features/user/domain/repositories/engine_settings.dart' import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/ui_helper.dart'; -class DeveloperSettingsScreen extends HookConsumerWidget { +class DeveloperSettingsScreen extends StatelessWidget { const DeveloperSettingsScreen(); @override - Widget build(BuildContext context, WidgetRef ref) { - final engineSettings = ref.watch(engineSettingsWithDefaultsProvider); - - final userAgentTextController = useTextEditingController( - text: engineSettings.userAgent, - keys: [engineSettings.userAgent], - ); - + Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Developer Settings')), body: FadingScroll( @@ -57,170 +50,16 @@ class DeveloperSettingsScreen extends HookConsumerWidget { builder: (context, controller) { return ListView( controller: controller, - children: [ - SwitchListTile.adaptive( - title: const Text('Enable JavaScript'), - subtitle: const Text( - 'While turning off JavaScript can boost security, privacy, and speed, it may cause some sites to not work as intended.', - ), - // ignore: deprecated_member_use use this icon for now - secondary: const Icon(MdiIcons.languageJavascript), - value: engineSettings.javascriptEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.javascriptEnabled(value), - ); - }, - ), - ListTile( - leading: const Icon(MdiIcons.cardAccountDetails), - title: TextField( - controller: userAgentTextController, - decoration: const InputDecoration( - labelText: 'Custom User Agent', - floatingLabelBehavior: FloatingLabelBehavior.always, - hintText: 'Mozilla/5.0 …', - ), - onSubmitted: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.userAgent(value), - ); - - if (context.mounted) { - final restart = await showUserAgentRestartDialog(context); - - if (restart == true) { - await exitApp(ref.container); - } - } - }, - ), - ), - SwitchListTile.adaptive( - title: const Text('Use third party CA certificates'), - subtitle: const Text( - 'Allows the use of third party certificates from the Android CA store', - ), - secondary: const Icon(MdiIcons.certificate), - value: engineSettings.enterpriseRootsEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .enterpriseRootsEnabled(value), - ); - }, - ), - ListTile( - title: const Text('Fingerprint Protection'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.fingerprint), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await FingerprintSettingsRoute().push(context); - }, - ), - CustomListTile( - title: 'Error Logs', - subtitle: 'Copy logs for issue reporting', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - Icons.bug_report, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await Clipboard.setData( - ClipboardData( - text: loggerMemory.buffer - .map((e) => e.lines.join('\n')) - .join('\n\n'), - ), - ); - - if (context.mounted) { - showInfoMessage(context, 'Logs copied'); - } - }, - icon: const Icon(Icons.copy), - label: const Text('Copy'), - ), - ), - if (kDebugMode) - CustomListTile( - title: 'Dart VM', - subtitle: 'Copy Dart VM service URL', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - Icons.bug_report, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - final serviceProtocolInfo = await Service.getInfo(); - - await Clipboard.setData( - ClipboardData( - text: - serviceProtocolInfo.serverUri?.toString() ?? - 'Error', - ), - ); - - if (context.mounted) { - showInfoMessage(context, 'Service URL copied'); - } - }, - icon: const Icon(Icons.copy), - label: const Text('Copy'), - ), - ), - ListTile( - leading: const Icon(MdiIcons.puzzle), - title: const Text('Custom Extension Collection'), - subtitle: const Text( - 'Custom Add-on Collections are curated lists of extensions that users can create and share.', - ), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await AddonCollectionRoute().push(context); - }, - ), - CustomListTile( - title: 'Reset UI', - subtitle: 'Rebuild the entire browser UI', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - Icons.bug_report, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () { - ref.read(appStateKeyProvider.notifier).reset(); - }, - icon: const Icon(Icons.restore), - label: const Text('Reset'), - ), - ), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _JavaScriptTile(), + _UserAgentTile(), + _EnterpriseRootsTile(), + _FingerprintProtectionTile(), + _ErrorLogsTile(), + _DartVmTile(), + _AddonCollectionTile(), + _ResetUITile(), ], ); }, @@ -228,3 +67,249 @@ class DeveloperSettingsScreen extends HookConsumerWidget { ); } } + +class _JavaScriptTile extends HookConsumerWidget { + const _JavaScriptTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final javascriptEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.javascriptEnabled, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Enable JavaScript'), + subtitle: const Text( + 'While turning off JavaScript can boost security, privacy, and speed, it may cause some sites to not work as intended.', + ), + // ignore: deprecated_member_use use this icon for now + secondary: const Icon(MdiIcons.languageJavascript), + value: javascriptEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.javascriptEnabled(value), + ); + }, + ); + } +} + +class _UserAgentTile extends HookConsumerWidget { + const _UserAgentTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userAgent = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.userAgent), + ); + + final userAgentTextController = useTextEditingController( + text: userAgent, + keys: [userAgent], + ); + + return ListTile( + leading: const Icon(MdiIcons.cardAccountDetails), + title: TextField( + controller: userAgentTextController, + decoration: const InputDecoration( + labelText: 'Custom User Agent', + floatingLabelBehavior: FloatingLabelBehavior.always, + hintText: 'Mozilla/5.0 …', + ), + onSubmitted: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith.userAgent(value), + ); + + if (context.mounted) { + final restart = await showUserAgentRestartDialog(context); + + if (restart == true) { + await exitApp(ref.container); + } + } + }, + ), + ); + } +} + +class _EnterpriseRootsTile extends HookConsumerWidget { + const _EnterpriseRootsTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enterpriseRootsEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.enterpriseRootsEnabled, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Use third party CA certificates'), + subtitle: const Text( + 'Allows the use of third party certificates from the Android CA store', + ), + secondary: const Icon(MdiIcons.certificate), + value: enterpriseRootsEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.enterpriseRootsEnabled(value), + ); + }, + ); + } +} + +class _FingerprintProtectionTile extends StatelessWidget { + const _FingerprintProtectionTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Fingerprint Protection'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.fingerprint), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await FingerprintSettingsRoute().push(context); + }, + ); + } +} + +class _ErrorLogsTile extends StatelessWidget { + const _ErrorLogsTile(); + + @override + Widget build(BuildContext context) { + return CustomListTile( + title: 'Error Logs', + subtitle: 'Copy logs for issue reporting', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.bug_report, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: loggerMemory.buffer + .map((e) => e.lines.join('\n')) + .join('\n\n'), + ), + ); + + if (context.mounted) { + showInfoMessage(context, 'Logs copied'); + } + }, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ); + } +} + +class _DartVmTile extends StatelessWidget { + const _DartVmTile(); + + @override + Widget build(BuildContext context) { + if (!kDebugMode) return const SizedBox.shrink(); + + return CustomListTile( + title: 'Dart VM', + subtitle: 'Copy Dart VM service URL', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.bug_report, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + final serviceProtocolInfo = await Service.getInfo(); + + await Clipboard.setData( + ClipboardData( + text: serviceProtocolInfo.serverUri?.toString() ?? 'Error', + ), + ); + + if (context.mounted) { + showInfoMessage(context, 'Service URL copied'); + } + }, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ); + } +} + +class _AddonCollectionTile extends StatelessWidget { + const _AddonCollectionTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(MdiIcons.puzzle), + title: const Text('Custom Extension Collection'), + subtitle: const Text( + 'Custom Add-on Collections are curated lists of extensions that users can create and share.', + ), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await AddonCollectionRoute().push(context); + }, + ); + } +} + +class _ResetUITile extends ConsumerWidget { + const _ResetUITile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return CustomListTile( + title: 'Reset UI', + subtitle: 'Rebuild the entire browser UI', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.bug_report, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () { + ref.read(appStateKeyProvider.notifier).reset(); + }, + icon: const Icon(Icons.restore), + label: const Text('Reset'), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index fd70dec2..29319471 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -35,13 +35,11 @@ import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/cache.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; -class GeneralSettingsScreen extends HookConsumerWidget { +class GeneralSettingsScreen extends StatelessWidget { const GeneralSettingsScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final generalSettings = ref.watch(generalSettingsWithDefaultsProvider); - + Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('General Settings')), body: FadingScroll( @@ -49,615 +47,26 @@ class GeneralSettingsScreen extends HookConsumerWidget { builder: (context, controller) { return ListView( controller: controller, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Theme'), - leading: Icon(Icons.palette), - contentPadding: EdgeInsets.zero, - ), - Center( - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.system, - icon: Icon(Icons.brightness_auto), - label: Text('System'), - ), - ButtonSegment( - value: ThemeMode.light, - icon: Icon(Icons.light_mode), - label: Text('Light'), - ), - ButtonSegment( - value: ThemeMode.dark, - icon: Icon(Icons.dark_mode), - label: Text('Dark'), - ), - ], - selected: {generalSettings.themeMode}, - onSelectionChanged: (value) async { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .themeMode(value.first), - ); - }, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('New Tab Default'), - subtitle: Text( - 'Choose the default type for manually created tabs', - ), - leading: Icon(MdiIcons.tab), - contentPadding: EdgeInsets.zero, - ), - Center( - child: SegmentedButton( - showSelectedIcon: false, - segments: const [ - ButtonSegment( - value: TabType.regular, - label: Text('Regular'), - icon: Icon(MdiIcons.tab), - ), - ButtonSegment( - value: TabType.private, - label: Text('Private'), - icon: Icon(MdiIcons.tabUnselected), - ), - ], - selected: {generalSettings.defaultCreateTabType}, - onSelectionChanged: (value) async { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .defaultCreateTabType(value.first), - ); - }, - style: switch (generalSettings.defaultCreateTabType) { - TabType.regular => null, - TabType.private => SegmentedButton.styleFrom( - selectedBackgroundColor: - AppColors.privateSelectionOverlay, - ), - TabType.child => null, - }, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('External Link Handling'), - subtitle: Text( - 'Choose how external links open in WebLibre', - ), - leading: Icon(MdiIcons.tabPlus), - contentPadding: EdgeInsets.zero, - ), - Center( - child: SegmentedButton( - showSelectedIcon: false, - segments: const [ - ButtonSegment( - value: TabIntentOpenSetting.ask, - label: Text('Prompt'), - icon: Icon(MdiIcons.messageQuestion), - ), - ButtonSegment( - value: TabIntentOpenSetting.regular, - label: Text('Regular'), - icon: Icon(MdiIcons.tab), - ), - ButtonSegment( - value: TabIntentOpenSetting.private, - label: Text('Private'), - icon: Icon(MdiIcons.tabUnselected), - ), - ], - selected: {generalSettings.tabIntentOpenSetting}, - onSelectionChanged: (value) async { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .tabIntentOpenSetting(value.first), - ); - }, - style: switch (generalSettings.tabIntentOpenSetting) { - TabIntentOpenSetting.regular => null, - TabIntentOpenSetting.private => - SegmentedButton.styleFrom( - selectedBackgroundColor: - AppColors.privateSelectionOverlay, - ), - TabIntentOpenSetting.ask => null, - }, - ), - ), - ], - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - title: Text('Default Search Provider'), - leading: Icon(MdiIcons.cloudSearch), - contentPadding: EdgeInsets.zero, - ), - Padding( - padding: EdgeInsets.only(left: 40), - child: DefaultSearchSelector(), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Default Autocomplete Provider'), - leading: Icon(MdiIcons.weatherCloudyArrowRight), - contentPadding: EdgeInsets.zero, - ), - Padding( - padding: const EdgeInsets.only(left: 40), - child: DropdownMenu( - initialSelection: - generalSettings.defaultSearchSuggestionsProvider, - inputDecorationTheme: InputDecorationTheme( - prefixIconConstraints: BoxConstraints.tight( - const Size.square(24), - ), - ), - width: double.infinity, - leadingIcon: generalSettings - .defaultSearchSuggestionsProvider - .relatedBang - .mapNotNull( - (trigger) => BangIcon(trigger: trigger), - ), - dropdownMenuEntries: SearchSuggestionProviders.values - .map((provider) { - return DropdownMenuEntry( - value: provider, - label: provider.label, - leadingIcon: provider.relatedBang.mapNotNull( - (trigger) => BangIcon(trigger: trigger), - ), - ); - }) - .toList(), - onSelected: (value) async { - if (value != null) { - await ref - .read( - saveGeneralSettingsControllerProvider - .notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .defaultSearchSuggestionsProvider(value), - ); - } - }, - ), - ), - ], - ), - ), - SwitchListTile.adaptive( - title: const Text('On Device AI'), - subtitle: const Text( - 'Local on-device features including container topic and tab suggestions', - ), - secondary: const Icon(MdiIcons.creation), - value: generalSettings.enableLocalAiFeatures, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .enableLocalAiFeatures(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Enable Reader Mode'), - subtitle: const Text( - 'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.', - ), - secondary: const Icon(MdiIcons.bookOpen), - value: generalSettings.enableReadability, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.enableReadability(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Enforce Reader Mode'), - subtitle: const Text( - 'Override readability probability of websites and always show Reader Mode capabilities even the site might not be compatible.', - ), - secondary: const Icon(MdiIcons.bookCheck), - value: - generalSettings.enableReadability && - generalSettings.enforceReadability, - onChanged: generalSettings.enableReadability - ? (value) async { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .enforceReadability(value), - ); - } - : null, - ), - SwitchListTile.adaptive( - title: const Text('Reader Mode in Tab Bar'), - subtitle: const Text( - 'Show reader mode button in the tab bar instead of only in the tab menu', - ), - secondary: const Icon(MdiIcons.bookHeart), - value: generalSettings.tabBarReaderView, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.tabBarReaderView(value), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Tab Bar Position'), - leading: Icon(MdiIcons.dockWindow), - contentPadding: EdgeInsets.zero, - ), - RadioGroup( - groupValue: generalSettings.tabBarPosition, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .tabBarPosition(value), - ); - } - }, - child: const Column( - children: [ - RadioListTile.adaptive( - value: TabBarPosition.top, - title: Text('Top'), - subtitle: Text( - 'Persistent tab bar without auto-hide', - ), - ), - RadioListTile.adaptive( - value: TabBarPosition.bottom, - title: Text('Bottom'), - subtitle: Text('Tab bar with auto-hide support'), - ), - ], - ), - ), - ], - ), - ), - - SwitchListTile.adaptive( - title: const Text('Show Contextual Tab Bar'), - subtitle: const Text( - 'Show additional bottom toolbar for navigation and actions', - ), - secondary: const Icon(MdiIcons.dockBottom), - value: generalSettings.tabBarShowContextualBar, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .tabBarShowContextualBar(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Show Quick Tab Switcher Bar'), - subtitle: const Text( - 'Show additional toolbar to quickly switch to recently used tabs', - ), - secondary: const Icon(MdiIcons.dockBottom), - value: generalSettings.tabBarShowQuickTabSwitcherBar, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .tabBarShowQuickTabSwitcherBar(value), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Quick Tab Switcher Mode'), - leading: Icon(MdiIcons.folderSettings), - contentPadding: EdgeInsets.zero, - ), - RadioGroup( - groupValue: generalSettings.quickTabSwitcherMode, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .quickTabSwitcherMode(value), - ); - } - }, - child: const Column( - children: [ - RadioListTile.adaptive( - value: QuickTabSwitcherMode.lastUsedTabs, - title: Text('Recently Used Tabs'), - subtitle: Text( - 'Recently used tabs across all containers', - ), - ), - RadioListTile.adaptive( - value: QuickTabSwitcherMode.containerTabs, - title: Text('Container Tabs'), - subtitle: Text( - 'Ordered tabs of the selected container', - ), - ), - ], - ), - ), - ], - ), - ), - SwitchListTile.adaptive( - title: const Text('Create Child Tabs'), - subtitle: const Text( - 'Display a button to create a child tab under the current tab (tree view only)', - ), - secondary: const Icon(MdiIcons.fileTree), - value: generalSettings.createChildTabsOption, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .createChildTabsOption(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Show Extension Shortcut'), - subtitle: const Text( - 'Display an extension menu directly on the tab bar', - ), - secondary: const Icon(MdiIcons.puzzleHeart), - value: generalSettings.showExtensionShortcut, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .showExtensionShortcut(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Auto Hide Tab Bar'), - subtitle: const Text('Hide tab bar when scrolling'), - secondary: const Icon(MdiIcons.folderHidden), - value: generalSettings.autoHideTabBar, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.autoHideTabBar(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Bottom Sheet Tab View'), - subtitle: const Text( - 'Display tabs in a bottom sheet instead of fullscreen', - ), - secondary: const Icon(MdiIcons.dockBottom), - value: generalSettings.tabViewBottomSheet, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.tabViewBottomSheet(value), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Tab Bar Swipe Behavior'), - leading: Icon(MdiIcons.gestureSwipeHorizontal), - contentPadding: EdgeInsets.zero, - ), - RadioGroup( - groupValue: generalSettings.tabBarSwipeAction, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .tabBarSwipeAction(value), - ); - } - }, - child: const Column( - children: [ - RadioListTile.adaptive( - value: TabBarSwipeAction.switchLastOpened, - title: Text('Switch to Last Used Tab'), - subtitle: Text( - 'Swipe to toggle between current and previously opened tab', - ), - ), - RadioListTile.adaptive( - value: TabBarSwipeAction.navigateOrderedTabs, - title: Text('Navigate Sequential Tabs'), - subtitle: Text( - 'Swipe left/right to move through tabs in order', - ), - ), - ], - ), - ), - ], - ), - ), - Consumer( - builder: (context, ref, child) { - final size = ref.watch( - iconCacheSizeMegabytesProvider.select( - (value) => value.value, - ), - ); - - return CustomListTile( - title: 'Icon Cache', - subtitle: 'Stored favicons', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - Icons.image, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - content: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: DefaultTextStyle( - style: GoogleFonts.robotoMono( - textStyle: DefaultTextStyle.of(context).style, - ), - child: Table( - columnWidths: const {0: FixedColumnWidth(100)}, - children: [ - TableRow( - children: [ - const Text('Size'), - Text('${size?.toStringAsFixed(2) ?? 0} MB'), - ], - ), - ], - ), - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await ref - .read(cacheRepositoryProvider.notifier) - .clearCache(); - }, - icon: const Icon(Icons.delete), - label: const Text('Clear'), - ), - ); - }, - ), + children: const [ + _ThemeSection(), + _NewTabDefaultSection(), + _ExternalLinkHandlingSection(), + _DefaultSearchProviderSection(), + _AutocompleteProviderSection(), + _OnDeviceAiTile(), + _EnableReaderModeTile(), + _EnforceReaderModeTile(), + _ReaderModeInTabBarTile(), + _TabBarPositionSection(), + _ShowContextualTabBarTile(), + _ShowQuickTabSwitcherBarTile(), + _QuickTabSwitcherModeSection(), + _CreateChildTabsTile(), + _ShowExtensionShortcutTile(), + _AutoHideTabBarTile(), + _BottomSheetTabViewTile(), + _TabBarSwipeBehaviorSection(), + _IconCacheTile(), ], ); }, @@ -665,3 +74,818 @@ class GeneralSettingsScreen extends HookConsumerWidget { ); } } + +class _ThemeSection extends HookConsumerWidget { + const _ThemeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.themeMode), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Theme'), + leading: Icon(Icons.palette), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.brightness_auto), + label: Text('System'), + ), + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode), + label: Text('Light'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode), + label: Text('Dark'), + ), + ], + selected: {themeMode}, + onSelectionChanged: (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.themeMode(value.first), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _NewTabDefaultSection extends HookConsumerWidget { + const _NewTabDefaultSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final defaultCreateTabType = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.defaultCreateTabType), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('New Tab Default'), + subtitle: Text( + 'Choose the default type for manually created tabs', + ), + leading: Icon(MdiIcons.tab), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + showSelectedIcon: false, + segments: const [ + ButtonSegment( + value: TabType.regular, + label: Text('Regular'), + icon: Icon(MdiIcons.tab), + ), + ButtonSegment( + value: TabType.private, + label: Text('Private'), + icon: Icon(MdiIcons.tabUnselected), + ), + ], + selected: {defaultCreateTabType}, + onSelectionChanged: (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .defaultCreateTabType(value.first), + ); + }, + style: switch (defaultCreateTabType) { + TabType.regular => null, + TabType.private => SegmentedButton.styleFrom( + selectedBackgroundColor: AppColors.privateSelectionOverlay, + ), + TabType.child => null, + }, + ), + ), + ], + ), + ); + } +} + +class _ExternalLinkHandlingSection extends HookConsumerWidget { + const _ExternalLinkHandlingSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabIntentOpenSetting = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabIntentOpenSetting), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('External Link Handling'), + subtitle: Text( + 'Choose how external links open in WebLibre', + ), + leading: Icon(MdiIcons.tabPlus), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + showSelectedIcon: false, + segments: const [ + ButtonSegment( + value: TabIntentOpenSetting.ask, + label: Text('Prompt'), + icon: Icon(MdiIcons.messageQuestion), + ), + ButtonSegment( + value: TabIntentOpenSetting.regular, + label: Text('Regular'), + icon: Icon(MdiIcons.tab), + ), + ButtonSegment( + value: TabIntentOpenSetting.private, + label: Text('Private'), + icon: Icon(MdiIcons.tabUnselected), + ), + ], + selected: {tabIntentOpenSetting}, + onSelectionChanged: (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .tabIntentOpenSetting(value.first), + ); + }, + style: switch (tabIntentOpenSetting) { + TabIntentOpenSetting.regular => null, + TabIntentOpenSetting.private => SegmentedButton.styleFrom( + selectedBackgroundColor: AppColors.privateSelectionOverlay, + ), + TabIntentOpenSetting.ask => null, + }, + ), + ), + ], + ), + ); + } +} + +class _DefaultSearchProviderSection extends StatelessWidget { + const _DefaultSearchProviderSection(); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text('Default Search Provider'), + leading: Icon(MdiIcons.cloudSearch), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: EdgeInsets.only(left: 40), + child: DefaultSearchSelector(), + ), + ], + ), + ); + } +} + +class _AutocompleteProviderSection extends HookConsumerWidget { + const _AutocompleteProviderSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final defaultSearchSuggestionsProvider = ref.watch( + generalSettingsWithDefaultsProvider + .select((s) => s.defaultSearchSuggestionsProvider), + ); + final relatedBang = defaultSearchSuggestionsProvider.relatedBang; + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Default Autocomplete Provider'), + leading: Icon(MdiIcons.weatherCloudyArrowRight), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40), + child: DropdownMenu( + initialSelection: defaultSearchSuggestionsProvider, + inputDecorationTheme: InputDecorationTheme( + prefixIconConstraints: BoxConstraints.tight( + const Size.square(24), + ), + ), + width: double.infinity, + leadingIcon: + relatedBang.mapNotNull((trigger) => BangIcon(trigger: trigger)), + dropdownMenuEntries: SearchSuggestionProviders.values + .map((provider) { + return DropdownMenuEntry( + value: provider, + label: provider.label, + leadingIcon: provider.relatedBang.mapNotNull( + (trigger) => BangIcon(trigger: trigger), + ), + ); + }) + .toList(), + onSelected: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .defaultSearchSuggestionsProvider(value), + ); + } + }, + ), + ), + ], + ), + ); + } +} + +class _OnDeviceAiTile extends HookConsumerWidget { + const _OnDeviceAiTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enableLocalAiFeatures = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.enableLocalAiFeatures), + ); + + return SwitchListTile.adaptive( + title: const Text('On Device AI'), + subtitle: const Text( + 'Local on-device features including container topic and tab suggestions', + ), + secondary: const Icon(MdiIcons.creation), + value: enableLocalAiFeatures, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.enableLocalAiFeatures(value), + ); + }, + ); + } +} + +class _EnableReaderModeTile extends HookConsumerWidget { + const _EnableReaderModeTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enableReadability = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.enableReadability), + ); + + return SwitchListTile.adaptive( + title: const Text('Enable Reader Mode'), + subtitle: const Text( + 'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.', + ), + secondary: const Icon(MdiIcons.bookOpen), + value: enableReadability, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.enableReadability(value), + ); + }, + ); + } +} + +class _EnforceReaderModeTile extends HookConsumerWidget { + const _EnforceReaderModeTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enableReadability = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.enableReadability), + ); + final enforceReadability = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.enforceReadability), + ); + + return SwitchListTile.adaptive( + title: const Text('Enforce Reader Mode'), + subtitle: const Text( + 'Override readability probability of websites and always show Reader Mode capabilities even the site might not be compatible.', + ), + secondary: const Icon(MdiIcons.bookCheck), + value: enableReadability && enforceReadability, + onChanged: enableReadability + ? (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.enforceReadability(value), + ); + } + : null, + ); + } +} + +class _ReaderModeInTabBarTile extends HookConsumerWidget { + const _ReaderModeInTabBarTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarReaderView = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabBarReaderView), + ); + + return SwitchListTile.adaptive( + title: const Text('Reader Mode in Tab Bar'), + subtitle: const Text( + 'Show reader mode button in the tab bar instead of only in the tab menu', + ), + secondary: const Icon(MdiIcons.bookHeart), + value: tabBarReaderView, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarReaderView(value), + ); + }, + ); + } +} + +class _TabBarPositionSection extends HookConsumerWidget { + const _TabBarPositionSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarPosition = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabBarPosition), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Tab Bar Position'), + leading: Icon(MdiIcons.dockWindow), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: tabBarPosition, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarPosition(value), + ); + } + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: TabBarPosition.top, + title: Text('Top'), + subtitle: Text( + 'Persistent tab bar without auto-hide', + ), + ), + RadioListTile.adaptive( + value: TabBarPosition.bottom, + title: Text('Bottom'), + subtitle: Text('Tab bar with auto-hide support'), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ShowContextualTabBarTile extends HookConsumerWidget { + const _ShowContextualTabBarTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarShowContextualBar = ref.watch( + generalSettingsWithDefaultsProvider + .select((s) => s.tabBarShowContextualBar), + ); + + return SwitchListTile.adaptive( + title: const Text('Show Contextual Tab Bar'), + subtitle: const Text( + 'Show additional bottom toolbar for navigation and actions', + ), + secondary: const Icon(MdiIcons.dockBottom), + value: tabBarShowContextualBar, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarShowContextualBar(value), + ); + }, + ); + } +} + +class _ShowQuickTabSwitcherBarTile extends HookConsumerWidget { + const _ShowQuickTabSwitcherBarTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarShowQuickTabSwitcherBar = ref.watch( + generalSettingsWithDefaultsProvider + .select((s) => s.tabBarShowQuickTabSwitcherBar), + ); + + return SwitchListTile.adaptive( + title: const Text('Show Quick Tab Switcher Bar'), + subtitle: const Text( + 'Show additional toolbar to quickly switch to recently used tabs', + ), + secondary: const Icon(MdiIcons.dockBottom), + value: tabBarShowQuickTabSwitcherBar, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .tabBarShowQuickTabSwitcherBar(value), + ); + }, + ); + } +} + +class _QuickTabSwitcherModeSection extends HookConsumerWidget { + const _QuickTabSwitcherModeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final quickTabSwitcherMode = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.quickTabSwitcherMode), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Quick Tab Switcher Mode'), + leading: Icon(MdiIcons.folderSettings), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: quickTabSwitcherMode, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .quickTabSwitcherMode(value), + ); + } + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: QuickTabSwitcherMode.lastUsedTabs, + title: Text('Recently Used Tabs'), + subtitle: Text( + 'Recently used tabs across all containers', + ), + ), + RadioListTile.adaptive( + value: QuickTabSwitcherMode.containerTabs, + title: Text('Container Tabs'), + subtitle: Text( + 'Ordered tabs of the selected container', + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _CreateChildTabsTile extends HookConsumerWidget { + const _CreateChildTabsTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final createChildTabsOption = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.createChildTabsOption), + ); + + return SwitchListTile.adaptive( + title: const Text('Create Child Tabs'), + subtitle: const Text( + 'Display a button to create a child tab under the current tab (tree view only)', + ), + secondary: const Icon(MdiIcons.fileTree), + value: createChildTabsOption, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.createChildTabsOption(value), + ); + }, + ); + } +} + +class _ShowExtensionShortcutTile extends HookConsumerWidget { + const _ShowExtensionShortcutTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showExtensionShortcut = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.showExtensionShortcut), + ); + + return SwitchListTile.adaptive( + title: const Text('Show Extension Shortcut'), + subtitle: const Text( + 'Display an extension menu directly on the tab bar', + ), + secondary: const Icon(MdiIcons.puzzleHeart), + value: showExtensionShortcut, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.showExtensionShortcut(value), + ); + }, + ); + } +} + +class _AutoHideTabBarTile extends HookConsumerWidget { + const _AutoHideTabBarTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final autoHideTabBar = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.autoHideTabBar), + ); + + return SwitchListTile.adaptive( + title: const Text('Auto Hide Tab Bar'), + subtitle: const Text('Hide tab bar when scrolling'), + secondary: const Icon(MdiIcons.folderHidden), + value: autoHideTabBar, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.autoHideTabBar(value), + ); + }, + ); + } +} + +class _BottomSheetTabViewTile extends HookConsumerWidget { + const _BottomSheetTabViewTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabViewBottomSheet = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabViewBottomSheet), + ); + + return SwitchListTile.adaptive( + title: const Text('Bottom Sheet Tab View'), + subtitle: const Text( + 'Display tabs in a bottom sheet instead of fullscreen', + ), + secondary: const Icon(MdiIcons.dockBottom), + value: tabViewBottomSheet, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabViewBottomSheet(value), + ); + }, + ); + } +} + +class _TabBarSwipeBehaviorSection extends HookConsumerWidget { + const _TabBarSwipeBehaviorSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarSwipeAction = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabBarSwipeAction), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Tab Bar Swipe Behavior'), + leading: Icon(MdiIcons.gestureSwipeHorizontal), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: tabBarSwipeAction, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarSwipeAction(value), + ); + } + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: TabBarSwipeAction.switchLastOpened, + title: Text('Switch to Last Used Tab'), + subtitle: Text( + 'Swipe to toggle between current and previously opened tab', + ), + ), + RadioListTile.adaptive( + value: TabBarSwipeAction.navigateOrderedTabs, + title: Text('Navigate Sequential Tabs'), + subtitle: Text( + 'Swipe left/right to move through tabs in order', + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _IconCacheTile extends HookConsumerWidget { + const _IconCacheTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final size = ref.watch( + iconCacheSizeMegabytesProvider.select((value) => value.value), + ); + + return CustomListTile( + title: 'Icon Cache', + subtitle: 'Stored favicons', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.image, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + content: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DefaultTextStyle( + style: GoogleFonts.robotoMono( + textStyle: DefaultTextStyle.of(context).style, + ), + child: Table( + columnWidths: const {0: FixedColumnWidth(100)}, + children: [ + TableRow( + children: [ + const Text('Size'), + Text('${size?.toStringAsFixed(2) ?? 0} MB'), + ], + ), + ], + ), + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + await ref.read(cacheRepositoryProvider.notifier).clearCache(); + }, + icon: const Icon(Icons.delete), + label: const Text('Clear'), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/web_engine_settings.dart b/app/lib/features/settings/presentation/screens/web_engine_settings.dart index 1ca6f9b8..6bd3f4f3 100644 --- a/app/lib/features/settings/presentation/screens/web_engine_settings.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_settings.dart @@ -31,14 +31,11 @@ import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; -class WebEngineSettingsScreen extends HookConsumerWidget { +class WebEngineSettingsScreen extends StatelessWidget { const WebEngineSettingsScreen(); @override - Widget build(BuildContext context, WidgetRef ref) { - final generalSettings = ref.watch(generalSettingsWithDefaultsProvider); - final engineSettings = ref.watch(engineSettingsWithDefaultsProvider); - + Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Web Engine Settings')), body: FadingScroll( @@ -46,463 +43,20 @@ class WebEngineSettingsScreen extends HookConsumerWidget { builder: (context, controller) { return ListView( controller: controller, - children: [ - SwitchListTile.adaptive( - title: const Text('Incognito Mode'), - subtitle: const Text( - 'Deletes selected browsing data upon app restart for enhanced privacy.', - ), - secondary: const Icon(MdiIcons.incognito), - value: generalSettings.deleteBrowsingDataOnQuit != null, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => value - ? currentSettings.copyWith.deleteBrowsingDataOnQuit( - {}, - ) - : currentSettings.copyWith.deleteBrowsingDataOnQuit( - null, - ), - ); - }, - ), - if (generalSettings.deleteBrowsingDataOnQuit != null) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - children: [ - for (final type in DeleteBrowsingDataType.values) - CheckboxListTile.adaptive( - value: generalSettings.deleteBrowsingDataOnQuit! - .contains(type), - controlAffinity: ListTileControlAffinity.leading, - title: Text(type.title), - subtitle: type.description.mapNotNull( - (description) => Text(description), - ), - onChanged: (value) async { - final notifier = ref.read( - saveGeneralSettingsControllerProvider.notifier, - ); - - if (value == true) { - await notifier.save( - (currentSettings) => currentSettings.copyWith - .deleteBrowsingDataOnQuit({ - ...currentSettings - .deleteBrowsingDataOnQuit!, - type, - }), - ); - } else { - await notifier.save( - (currentSettings) => currentSettings.copyWith - .deleteBrowsingDataOnQuit( - { - ...currentSettings - .deleteBrowsingDataOnQuit!, - }..remove(type), - ), - ); - } - }, - ), - ], - ), - ), - ListTile( - title: const Text('Delete Browsing Data'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.databaseRemove), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return const DeleteDataDialog(initialSettings: {}); - }, - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Auto-Clear History'), - subtitle: Text( - 'Automatically delete browsing history older than the selected time period', - ), - leading: Icon(MdiIcons.deleteClock), - contentPadding: EdgeInsets.zero, - ), - Padding( - padding: const EdgeInsets.only(left: 40.0), - child: DropdownMenu( - initialSelection: - generalSettings.historyAutoCleanInterval, - inputDecorationTheme: InputDecorationTheme( - prefixIconConstraints: BoxConstraints.tight( - const Size.square(24), - ), - ), - width: double.infinity, - dropdownMenuEntries: const [ - DropdownMenuEntry( - value: Duration.zero, - label: 'Never', - ), - DropdownMenuEntry( - value: Duration(days: 1), - label: '1 Day', - ), - DropdownMenuEntry( - value: Duration(days: 7), - label: '1 Week', - ), - DropdownMenuEntry( - value: Duration(days: 14), - label: '2 Weeks', - ), - DropdownMenuEntry( - value: Duration(days: 30), - label: '1 Month', - ), - DropdownMenuEntry( - value: Duration(days: 90), - label: '3 Months', - ), - ], - onSelected: (value) async { - await ref - .read( - saveGeneralSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .historyAutoCleanInterval( - value ?? Duration.zero, - ), - ); - }, - ), - ), - ], - ), - ), - ListTile( - title: const Text('Browser Languages'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.translate), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await LocaleSettingsRoute().push(context); - }, - ), - SwitchListTile.adaptive( - title: const Text('Global Privacy Control (GPC)'), - secondary: const Icon(MdiIcons.incognitoCircleOff), - value: engineSettings.globalPrivacyControlEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .globalPrivacyControlEnabled(value), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Block insecure HTTP connections'), - leading: Icon(MdiIcons.lockOpen), - contentPadding: EdgeInsets.zero, - ), - Center( - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: HttpsOnlyMode.disabled, - label: Text('Disabled'), - ), - ButtonSegment( - value: HttpsOnlyMode.enabled, - label: Text('Enabled'), - ), - ButtonSegment( - value: HttpsOnlyMode.privateOnly, - label: Text('Private mode only'), - ), - ], - selected: {engineSettings.httpsOnlyMode}, - onSelectionChanged: (value) async { - await ref - .read( - saveEngineSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .httpsOnlyMode(value.first), - ); - }, - ), - ), - ], - ), - ), - ListTile( - title: const Text('DNS over HTTPS'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.dns), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await DohSettingsRoute().push(context); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Enhanced Tracking Protection'), - leading: Icon(MdiIcons.incognitoCircleOff), - contentPadding: EdgeInsets.zero, - ), - RadioGroup( - groupValue: engineSettings.trackingProtectionPolicy, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .trackingProtectionPolicy(value), - ); - }, - child: const Column( - children: [ - RadioListTile.adaptive( - value: TrackingProtectionPolicy.none, - title: Text('Disabled'), - ), - RadioListTile.adaptive( - value: TrackingProtectionPolicy.recommended, - title: Text('Standard'), - subtitle: Text( - 'Pages will load normally, but block fewer trackers.', - ), - ), - RadioListTile.adaptive( - value: TrackingProtectionPolicy.strict, - title: Text('Strict'), - subtitle: Text( - 'Stronger tracking protection and faster performance, but some sites may not work properly.', - ), - ), - RadioListTile.adaptive( - value: TrackingProtectionPolicy.custom, - title: Text('Custom'), - subtitle: Text( - 'Choose which trackers and scripts to block.', - ), - ), - ], - ), - ), - ], - ), - ), - // Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 16.0, - // vertical: 8, - // ), - // child: Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // const ListTile( - // title: Text('Cookie Banner Blocker'), - // leading: Icon(MdiIcons.cookieSettings), - // contentPadding: EdgeInsets.zero, - // ), - // CheckboxListTile.adaptive( - // value: - // engineSettings.cookieBannerHandlingMode == - // CookieBannerHandlingMode.rejectAll, - // title: const Text('In normal browsing'), - // controlAffinity: ListTileControlAffinity.leading, - // onChanged: (value) async { - // await ref - // .read(saveEngineSettingsControllerProvider.notifier) - // .save( - // (currentSettings) => currentSettings.copyWith - // .cookieBannerHandlingMode( - // value! - // ? CookieBannerHandlingMode.rejectAll - // : CookieBannerHandlingMode.disabled, - // ), - // ); - // }, - // ), - // CheckboxListTile.adaptive( - // value: - // engineSettings - // .cookieBannerHandlingModePrivateBrowsing == - // CookieBannerHandlingMode.rejectAll, - // title: const Text('In private browsing'), - // controlAffinity: ListTileControlAffinity.leading, - // onChanged: (value) async { - // await ref - // .read(saveEngineSettingsControllerProvider.notifier) - // .save( - // (currentSettings) => currentSettings.copyWith - // .cookieBannerHandlingModePrivateBrowsing( - // value! - // ? CookieBannerHandlingMode.rejectAll - // : CookieBannerHandlingMode.disabled, - // ), - // ); - // }, - // ), - // ], - // ), - // ), - SwitchListTile.adaptive( - title: const Text('Bounce Tracking Protection'), - subtitle: const Text( - 'Blocks redirect trackers that collect data through intermediate URL redirects between websites', - ), - secondary: const Icon(MdiIcons.securityNetwork), - value: switch (engineSettings - .contentBlocking - .bounceTrackingProtectionMode) { - BounceTrackingProtectionMode.disabled => false, - BounceTrackingProtectionMode.enabled => true, - BounceTrackingProtectionMode.enabledStandby => false, - BounceTrackingProtectionMode.enabledDryRun => false, - }, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .bounceTrackingProtectionMode( - value - ? BounceTrackingProtectionMode.enabled - : BounceTrackingProtectionMode.disabled, - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('Query Parameter Stripping'), - subtitle: Text( - 'Removes tracking parameters from URLs to prevent cross-site user tracking', - ), - leading: Icon(MdiIcons.closeNetwork), - contentPadding: EdgeInsets.zero, - ), - Center( - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: QueryParameterStripping.disabled, - label: Text('Disabled'), - ), - ButtonSegment( - value: QueryParameterStripping.enabled, - label: Text('Enabled'), - ), - ButtonSegment( - value: QueryParameterStripping.privateOnly, - label: Text('Private mode only'), - ), - ], - selected: {engineSettings.queryParameterStripping}, - onSelectionChanged: (value) async { - await ref - .read( - saveEngineSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .queryParameterStripping(value.first), - ); - }, - ), - ), - ], - ), - ), - SwitchListTile.adaptive( - title: const Text('Built-in PDF Viewer'), - subtitle: const Text( - 'Open PDF files directly in the browser without downloading', - ), - secondary: const Icon(MdiIcons.filePdfBox), - value: engineSettings.enablePdfJs, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.enablePdfJs(value), - ); - }, - ), - ListTile( - title: const Text('Web Engine Hardening'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.shieldLock), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await WebEngineHardeningRoute().push(context); - }, - ), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _IncognitoModeSection(), + _DeleteBrowsingDataTile(), + _AutoClearHistorySection(), + _BrowserLanguagesTile(), + _GlobalPrivacyControlTile(), + _HttpsOnlyModeSection(), + _DnsTile(), + _TrackingProtectionSection(), + _BounceTrackingProtectionTile(), + _QueryParameterStrippingSection(), + _PdfViewerTile(), + _WebEngineHardeningTile(), ], ); }, @@ -510,3 +64,555 @@ class WebEngineSettingsScreen extends HookConsumerWidget { ); } } + +class _IncognitoModeSection extends HookConsumerWidget { + const _IncognitoModeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final deleteBrowsingDataOnQuit = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.deleteBrowsingDataOnQuit, + ), + ); + + return Column( + children: [ + SwitchListTile.adaptive( + title: const Text('Incognito Mode'), + subtitle: const Text( + 'Deletes selected browsing data upon app restart for enhanced privacy.', + ), + secondary: const Icon(MdiIcons.incognito), + value: deleteBrowsingDataOnQuit != null, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => value + ? currentSettings.copyWith.deleteBrowsingDataOnQuit( + {}, + ) + : currentSettings.copyWith.deleteBrowsingDataOnQuit( + null, + ), + ); + }, + ), + if (deleteBrowsingDataOnQuit != null) + _DeleteBrowsingDataTypes( + selectedTypes: deleteBrowsingDataOnQuit, + ), + ], + ); + } +} + +class _DeleteBrowsingDataTypes extends HookConsumerWidget { + final Set selectedTypes; + + const _DeleteBrowsingDataTypes({required this.selectedTypes}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + children: [ + for (final type in DeleteBrowsingDataType.values) + CheckboxListTile.adaptive( + value: selectedTypes.contains(type), + controlAffinity: ListTileControlAffinity.leading, + title: Text(type.title), + subtitle: type.description.mapNotNull( + (description) => Text(description), + ), + onChanged: (value) async { + final notifier = ref.read( + saveGeneralSettingsControllerProvider.notifier, + ); + + if (value == true) { + await notifier.save( + (currentSettings) => currentSettings.copyWith + .deleteBrowsingDataOnQuit({ + ...currentSettings.deleteBrowsingDataOnQuit!, + type, + }), + ); + } else { + await notifier.save( + (currentSettings) => currentSettings.copyWith + .deleteBrowsingDataOnQuit( + { + ...currentSettings.deleteBrowsingDataOnQuit!, + }..remove(type), + ), + ); + } + }, + ), + ], + ), + ); + } +} + +class _DeleteBrowsingDataTile extends StatelessWidget { + const _DeleteBrowsingDataTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Delete Browsing Data'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.databaseRemove), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return const DeleteDataDialog(initialSettings: {}); + }, + ); + }, + ); + } +} + +class _AutoClearHistorySection extends HookConsumerWidget { + const _AutoClearHistorySection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyAutoCleanInterval = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.historyAutoCleanInterval, + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Auto-Clear History'), + subtitle: Text( + 'Automatically delete browsing history older than the selected time period', + ), + leading: Icon(MdiIcons.deleteClock), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40.0), + child: DropdownMenu( + initialSelection: historyAutoCleanInterval, + inputDecorationTheme: InputDecorationTheme( + prefixIconConstraints: BoxConstraints.tight( + const Size.square(24), + ), + ), + width: double.infinity, + dropdownMenuEntries: const [ + DropdownMenuEntry( + value: Duration.zero, + label: 'Never', + ), + DropdownMenuEntry( + value: Duration(days: 1), + label: '1 Day', + ), + DropdownMenuEntry( + value: Duration(days: 7), + label: '1 Week', + ), + DropdownMenuEntry( + value: Duration(days: 14), + label: '2 Weeks', + ), + DropdownMenuEntry( + value: Duration(days: 30), + label: '1 Month', + ), + DropdownMenuEntry( + value: Duration(days: 90), + label: '3 Months', + ), + ], + onSelected: (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .historyAutoCleanInterval( + value ?? Duration.zero, + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _BrowserLanguagesTile extends StatelessWidget { + const _BrowserLanguagesTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Browser Languages'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.translate), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await LocaleSettingsRoute().push(context); + }, + ); + } +} + +class _GlobalPrivacyControlTile extends HookConsumerWidget { + const _GlobalPrivacyControlTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final globalPrivacyControlEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.globalPrivacyControlEnabled, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Global Privacy Control (GPC)'), + secondary: const Icon(MdiIcons.incognitoCircleOff), + value: globalPrivacyControlEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.globalPrivacyControlEnabled(value), + ); + }, + ); + } +} + +class _HttpsOnlyModeSection extends HookConsumerWidget { + const _HttpsOnlyModeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final httpsOnlyMode = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.httpsOnlyMode), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Block insecure HTTP connections'), + leading: Icon(MdiIcons.lockOpen), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: HttpsOnlyMode.disabled, + label: Text('Disabled'), + ), + ButtonSegment( + value: HttpsOnlyMode.enabled, + label: Text('Enabled'), + ), + ButtonSegment( + value: HttpsOnlyMode.privateOnly, + label: Text('Private mode only'), + ), + ], + selected: {httpsOnlyMode}, + onSelectionChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.httpsOnlyMode(value.first), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _DnsTile extends StatelessWidget { + const _DnsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('DNS over HTTPS'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.dns), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await DohSettingsRoute().push(context); + }, + ); + } +} + +class _TrackingProtectionSection extends HookConsumerWidget { + const _TrackingProtectionSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trackingProtectionPolicy = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.trackingProtectionPolicy, + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Enhanced Tracking Protection'), + leading: Icon(MdiIcons.incognitoCircleOff), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: trackingProtectionPolicy, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.trackingProtectionPolicy(value), + ); + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: TrackingProtectionPolicy.none, + title: Text('Disabled'), + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.recommended, + title: Text('Standard'), + subtitle: Text( + 'Pages will load normally, but block fewer trackers.', + ), + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.strict, + title: Text('Strict'), + subtitle: Text( + 'Stronger tracking protection and faster performance, but some sites may not work properly.', + ), + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.custom, + title: Text('Custom'), + subtitle: Text( + 'Choose which trackers and scripts to block.', + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _BounceTrackingProtectionTile extends HookConsumerWidget { + const _BounceTrackingProtectionTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bounceTrackingProtectionMode = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.contentBlocking.bounceTrackingProtectionMode, + ), + ); + + final isEnabled = switch (bounceTrackingProtectionMode) { + BounceTrackingProtectionMode.disabled => false, + BounceTrackingProtectionMode.enabled => true, + BounceTrackingProtectionMode.enabledStandby => false, + BounceTrackingProtectionMode.enabledDryRun => false, + }; + + return SwitchListTile.adaptive( + title: const Text('Bounce Tracking Protection'), + subtitle: const Text( + 'Blocks redirect trackers that collect data through intermediate URL redirects between websites', + ), + secondary: const Icon(MdiIcons.securityNetwork), + value: isEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .bounceTrackingProtectionMode( + value + ? BounceTrackingProtectionMode.enabled + : BounceTrackingProtectionMode.disabled, + ), + ); + }, + ); + } +} + +class _QueryParameterStrippingSection extends HookConsumerWidget { + const _QueryParameterStrippingSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final queryParameterStripping = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.queryParameterStripping, + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Query Parameter Stripping'), + subtitle: Text( + 'Removes tracking parameters from URLs to prevent cross-site user tracking', + ), + leading: Icon(MdiIcons.closeNetwork), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: QueryParameterStripping.disabled, + label: Text('Disabled'), + ), + ButtonSegment( + value: QueryParameterStripping.enabled, + label: Text('Enabled'), + ), + ButtonSegment( + value: QueryParameterStripping.privateOnly, + label: Text('Private mode only'), + ), + ], + selected: {queryParameterStripping}, + onSelectionChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .queryParameterStripping(value.first), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _PdfViewerTile extends HookConsumerWidget { + const _PdfViewerTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enablePdfJs = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.enablePdfJs), + ); + + return SwitchListTile.adaptive( + title: const Text('Built-in PDF Viewer'), + subtitle: const Text( + 'Open PDF files directly in the browser without downloading', + ), + secondary: const Icon(MdiIcons.filePdfBox), + value: enablePdfJs, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith.enablePdfJs(value), + ); + }, + ); + } +} + +class _WebEngineHardeningTile extends StatelessWidget { + const _WebEngineHardeningTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Web Engine Hardening'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.shieldLock), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await WebEngineHardeningRoute().push(context); + }, + ); + } +}