diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 0d2730da..8609f7ae 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -53,16 +53,19 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/c import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart'; import 'package:weblibre/features/onboarding/presentation/onboarding.dart'; import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart'; +import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/appearance_display_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart'; -import 'package:weblibre/features/settings/presentation/screens/developer_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart'; -import 'package:weblibre/features/settings/presentation/screens/general_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/fingerprinting_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/search_content_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart'; -import 'package:weblibre/features/settings/presentation/screens/web_engine_settings.dart'; import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart'; import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart'; import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 8ecc7dbf..154852f6 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -80,9 +80,34 @@ RouteBase get $settingsRoute => GoRouteData.$route( factory: $SettingsRoute._fromState, routes: [ GoRouteData.$route( - path: 'general', - name: 'GeneralSettingsRoute', - factory: $GeneralSettingsRoute._fromState, + path: 'appearance_display', + name: 'AppearanceDisplaySettingsRoute', + factory: $AppearanceDisplaySettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'privacy_security', + name: 'PrivacySecuritySettingsRoute', + factory: $PrivacySecuritySettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'search_content', + name: 'SearchContentSettingsRoute', + factory: $SearchContentSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'tabs_behavior', + name: 'TabsBehaviorSettingsRoute', + factory: $TabsBehaviorSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'fingerprinting', + name: 'FingerprintingSettingsRoute', + factory: $FingerprintingSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'advanced', + name: 'AdvancedSettingsRoute', + factory: $AdvancedSettingsRoute._fromState, ), GoRouteData.$route( path: 'bang', @@ -90,50 +115,36 @@ RouteBase get $settingsRoute => GoRouteData.$route( factory: $BangSettingsRoute._fromState, ), GoRouteData.$route( - path: 'web_engine', - name: 'WebEngineSettingsRoute', - factory: $WebEngineSettingsRoute._fromState, + path: 'hardening', + name: 'WebEngineHardeningRoute', + factory: $WebEngineHardeningRoute._fromState, routes: [ GoRouteData.$route( - path: 'hardening', - name: 'WebEngineHardeningRoute', - factory: $WebEngineHardeningRoute._fromState, - routes: [ - GoRouteData.$route( - path: 'group/:group', - name: 'WebEngineHardeningGroupRoute', - factory: $WebEngineHardeningGroupRoute._fromState, - ), - ], - ), - GoRouteData.$route( - path: 'doh', - name: 'DohSettingsRoute', - factory: $DohSettingsRoute._fromState, - ), - GoRouteData.$route( - path: 'fingerprint', - name: 'FingerprintSettingsRoute', - factory: $FingerprintSettingsRoute._fromState, - ), - GoRouteData.$route( - path: 'locales', - name: 'LocaleSettingsRoute', - factory: $LocaleSettingsRoute._fromState, + path: 'group/:group', + name: 'WebEngineHardeningGroupRoute', + factory: $WebEngineHardeningGroupRoute._fromState, ), ], ), GoRouteData.$route( - path: 'developer', - name: 'DeveloperSettingsRoute', - factory: $DeveloperSettingsRoute._fromState, - routes: [ - GoRouteData.$route( - path: 'addon_collection', - name: 'AddonCollectionRoute', - factory: $AddonCollectionRoute._fromState, - ), - ], + path: 'doh', + name: 'DohSettingsRoute', + factory: $DohSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'fingerprint', + name: 'FingerprintSettingsRoute', + factory: $FingerprintSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'locales', + name: 'LocaleSettingsRoute', + factory: $LocaleSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'addon_collection', + name: 'AddonCollectionRoute', + factory: $AddonCollectionRoute._fromState, ), ], ); @@ -158,12 +169,117 @@ mixin $SettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $GeneralSettingsRoute on GoRouteData { - static GeneralSettingsRoute _fromState(GoRouterState state) => - GeneralSettingsRoute(); +mixin $AppearanceDisplaySettingsRoute on GoRouteData { + static AppearanceDisplaySettingsRoute _fromState(GoRouterState state) => + AppearanceDisplaySettingsRoute(); @override - String get location => GoRouteData.$location('/settings/general'); + String get location => GoRouteData.$location('/settings/appearance_display'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $PrivacySecuritySettingsRoute on GoRouteData { + static PrivacySecuritySettingsRoute _fromState(GoRouterState state) => + PrivacySecuritySettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/privacy_security'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $SearchContentSettingsRoute on GoRouteData { + static SearchContentSettingsRoute _fromState(GoRouterState state) => + SearchContentSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/search_content'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $TabsBehaviorSettingsRoute on GoRouteData { + static TabsBehaviorSettingsRoute _fromState(GoRouterState state) => + TabsBehaviorSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/tabs_behavior'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $FingerprintingSettingsRoute on GoRouteData { + static FingerprintingSettingsRoute _fromState(GoRouterState state) => + FingerprintingSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/fingerprinting'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $AdvancedSettingsRoute on GoRouteData { + static AdvancedSettingsRoute _fromState(GoRouterState state) => + AdvancedSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/advanced'); @override void go(BuildContext context) => context.go(location); @@ -200,34 +316,12 @@ mixin $BangSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $WebEngineSettingsRoute on GoRouteData { - static WebEngineSettingsRoute _fromState(GoRouterState state) => - WebEngineSettingsRoute(); - - @override - String get location => GoRouteData.$location('/settings/web_engine'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - mixin $WebEngineHardeningRoute on GoRouteData { static WebEngineHardeningRoute _fromState(GoRouterState state) => WebEngineHardeningRoute(); @override - String get location => - GoRouteData.$location('/settings/web_engine/hardening'); + String get location => GoRouteData.$location('/settings/hardening'); @override void go(BuildContext context) => context.go(location); @@ -252,7 +346,7 @@ mixin $WebEngineHardeningGroupRoute on GoRouteData { @override String get location => GoRouteData.$location( - '/settings/web_engine/hardening/group/${Uri.encodeComponent(_self.group)}', + '/settings/hardening/group/${Uri.encodeComponent(_self.group)}', ); @override @@ -273,7 +367,7 @@ mixin $DohSettingsRoute on GoRouteData { static DohSettingsRoute _fromState(GoRouterState state) => DohSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/web_engine/doh'); + String get location => GoRouteData.$location('/settings/doh'); @override void go(BuildContext context) => context.go(location); @@ -294,8 +388,7 @@ mixin $FingerprintSettingsRoute on GoRouteData { FingerprintSettingsRoute(); @override - String get location => - GoRouteData.$location('/settings/web_engine/fingerprint'); + String get location => GoRouteData.$location('/settings/fingerprint'); @override void go(BuildContext context) => context.go(location); @@ -316,28 +409,7 @@ mixin $LocaleSettingsRoute on GoRouteData { LocaleSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/web_engine/locales'); - - @override - void go(BuildContext context) => context.go(location); - - @override - Future push(BuildContext context) => context.push(location); - - @override - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - @override - void replace(BuildContext context) => context.replace(location); -} - -mixin $DeveloperSettingsRoute on GoRouteData { - static DeveloperSettingsRoute _fromState(GoRouterState state) => - DeveloperSettingsRoute(); - - @override - String get location => GoRouteData.$location('/settings/developer'); + String get location => GoRouteData.$location('/settings/locales'); @override void go(BuildContext context) => context.go(location); @@ -358,8 +430,7 @@ mixin $AddonCollectionRoute on GoRouteData { AddonCollectionRoute(); @override - String get location => - GoRouteData.$location('/settings/developer/addon_collection'); + String get location => GoRouteData.$location('/settings/addon_collection'); @override void go(BuildContext context) => context.go(location); diff --git a/app/lib/core/routing/routes.settings.dart b/app/lib/core/routing/routes.settings.dart index 3bac52aa..46c21c36 100644 --- a/app/lib/core/routing/routes.settings.dart +++ b/app/lib/core/routing/routes.settings.dart @@ -23,45 +23,53 @@ part of 'routes.dart'; name: 'SettingsRoute', path: '/settings', routes: [ - TypedGoRoute( - name: 'GeneralSettingsRoute', - path: 'general', + TypedGoRoute( + name: 'AppearanceDisplaySettingsRoute', + path: 'appearance_display', + ), + TypedGoRoute( + name: 'PrivacySecuritySettingsRoute', + path: 'privacy_security', + ), + TypedGoRoute( + name: 'SearchContentSettingsRoute', + path: 'search_content', + ), + TypedGoRoute( + name: 'TabsBehaviorSettingsRoute', + path: 'tabs_behavior', + ), + TypedGoRoute( + name: 'FingerprintingSettingsRoute', + path: 'fingerprinting', + ), + TypedGoRoute( + name: 'AdvancedSettingsRoute', + path: 'advanced', ), TypedGoRoute(name: 'BangSettingsRoute', path: 'bang'), - TypedGoRoute( - name: 'WebEngineSettingsRoute', - path: 'web_engine', + TypedGoRoute( + name: 'WebEngineHardeningRoute', + path: 'hardening', routes: [ - TypedGoRoute( - name: 'WebEngineHardeningRoute', - path: 'hardening', - routes: [ - TypedGoRoute( - name: 'WebEngineHardeningGroupRoute', - path: 'group/:group', - ), - ], - ), - TypedGoRoute(name: 'DohSettingsRoute', path: 'doh'), - TypedGoRoute( - name: 'FingerprintSettingsRoute', - path: 'fingerprint', - ), - TypedGoRoute( - name: 'LocaleSettingsRoute', - path: 'locales', + TypedGoRoute( + name: 'WebEngineHardeningGroupRoute', + path: 'group/:group', ), ], ), - TypedGoRoute( - name: 'DeveloperSettingsRoute', - path: 'developer', - routes: [ - TypedGoRoute( - name: 'AddonCollectionRoute', - path: 'addon_collection', - ), - ], + TypedGoRoute(name: 'DohSettingsRoute', path: 'doh'), + TypedGoRoute( + name: 'FingerprintSettingsRoute', + path: 'fingerprint', + ), + TypedGoRoute( + name: 'LocaleSettingsRoute', + path: 'locales', + ), + TypedGoRoute( + name: 'AddonCollectionRoute', + path: 'addon_collection', ), ], ) @@ -72,10 +80,50 @@ class SettingsRoute extends GoRouteData with $SettingsRoute { } } -class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute { +class AppearanceDisplaySettingsRoute extends GoRouteData + with $AppearanceDisplaySettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const GeneralSettingsScreen(); + return const AppearanceDisplaySettingsScreen(); + } +} + +class PrivacySecuritySettingsRoute extends GoRouteData + with $PrivacySecuritySettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const PrivacySecuritySettingsScreen(); + } +} + +class SearchContentSettingsRoute extends GoRouteData + with $SearchContentSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const SearchContentSettingsScreen(); + } +} + +class TabsBehaviorSettingsRoute extends GoRouteData + with $TabsBehaviorSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const TabsBehaviorSettingsScreen(); + } +} + +class FingerprintingSettingsRoute extends GoRouteData + with $FingerprintingSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const FingerprintingSettingsScreen(); + } +} + +class AdvancedSettingsRoute extends GoRouteData with $AdvancedSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const AdvancedSettingsScreen(); } } @@ -108,20 +156,6 @@ class LocaleSettingsRoute extends GoRouteData with $LocaleSettingsRoute { } } -class WebEngineSettingsRoute extends GoRouteData with $WebEngineSettingsRoute { - @override - Widget build(BuildContext context, GoRouterState state) { - return const WebEngineSettingsScreen(); - } -} - -class DeveloperSettingsRoute extends GoRouteData with $DeveloperSettingsRoute { - @override - Widget build(BuildContext context, GoRouterState state) { - return const DeveloperSettingsScreen(); - } -} - class AddonCollectionRoute extends GoRouteData with $AddonCollectionRoute { @override Widget build(BuildContext context, GoRouterState state) { diff --git a/app/lib/features/settings/presentation/screens/developer_settings.dart b/app/lib/features/settings/presentation/screens/advanced_settings.dart similarity index 73% rename from app/lib/features/settings/presentation/screens/developer_settings.dart rename to app/lib/features/settings/presentation/screens/advanced_settings.dart index bead7023..2934896a 100644 --- a/app/lib/features/settings/presentation/screens/developer_settings.dart +++ b/app/lib/features/settings/presentation/screens/advanced_settings.dart @@ -25,45 +25,90 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/app_state.dart'; -import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; import 'package:weblibre/features/settings/presentation/dialogs/user_agent_restart_dialog.dart'; import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; import 'package:weblibre/features/user/data/models/engine_settings.dart'; +import 'package:weblibre/features/user/domain/providers.dart'; +import 'package:weblibre/features/user/domain/repositories/cache.dart'; 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 StatelessWidget { - const DeveloperSettingsScreen(); +class AdvancedSettingsScreen extends StatelessWidget { + const AdvancedSettingsScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Developer Settings')), - body: FadingScroll( - fadingSize: 25, - builder: (context, controller) { + appBar: AppBar(title: const Text('Advanced')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { return ListView( controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _JavaScriptTile(), - _UserAgentTile(), - _EnterpriseRootsTile(), - _FingerprintProtectionTile(), - _ErrorLogsTile(), - _DartVmTile(), - _AddonCollectionTile(), - _ResetUITile(), + _ContentBehaviorSection(), + _StorageDebuggingSection(), + _ResetSection(), ], ); }, ), + ), + ); + } +} + +class _ContentBehaviorSection extends StatelessWidget { + const _ContentBehaviorSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Content Behavior'), + _JavaScriptTile(), + _UserAgentTile(), + _EnterpriseRootsTile(), + ], + ); + } +} + +class _StorageDebuggingSection extends StatelessWidget { + const _StorageDebuggingSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Storage & Debugging'), + _IconCacheTile(), + _ErrorLogsTile(), + _DartVmTile(), + ], + ); + } +} + +class _ResetSection extends StatelessWidget { + const _ResetSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Reset'), + _ResetUITile(), + ], ); } } @@ -172,22 +217,52 @@ class _EnterpriseRootsTile extends HookConsumerWidget { } } -class _FingerprintProtectionTile extends StatelessWidget { - const _FingerprintProtectionTile(); +class _IconCacheTile extends HookConsumerWidget { + const _IconCacheTile(); @override - Widget build(BuildContext context) { - return ListTile( - title: const Text('Fingerprint Protection'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, + 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'), ), - leading: const Icon(MdiIcons.fingerprint), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await FingerprintSettingsRoute().push(context); - }, ); } } @@ -268,25 +343,6 @@ class _DartVmTile extends StatelessWidget { } } -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(); diff --git a/app/lib/features/settings/presentation/screens/appearance_display_settings.dart b/app/lib/features/settings/presentation/screens/appearance_display_settings.dart new file mode 100644 index 00000000..23ae8bba --- /dev/null +++ b/app/lib/features/settings/presentation/screens/appearance_display_settings.dart @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; + +class AppearanceDisplaySettingsScreen extends StatelessWidget { + const AppearanceDisplaySettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Appearance & Display')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _VisualSection(), + _TabBarLayoutSection(), + _GesturesSection(), + ], + ); + }, + ), + ), + ); + } +} + +class _VisualSection extends StatelessWidget { + const _VisualSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Visual'), + _ThemeSection(), + ], + ); + } +} + +class _TabBarLayoutSection extends StatelessWidget { + const _TabBarLayoutSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Tab Bar Layout'), + _TabBarPositionSection(), + _ShowContextualTabBarTile(), + _AutoHideTabBarTile(), + _BottomSheetTabViewTile(), + _ShowQuickTabSwitcherBarTile(), + _QuickTabSwitcherModeSection(), + ], + ); + } +} + +class _GesturesSection extends StatelessWidget { + const _GesturesSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Gestures'), + _PullToRefreshTile(), + _DoubleBackCloseTabTile(), + ], + ); + } +} + +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 _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 _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 _PullToRefreshTile extends HookConsumerWidget { + const _PullToRefreshTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pullToRefreshEnabled = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled), + ); + + return SwitchListTile.adaptive( + title: const Text('Pull to Refresh'), + subtitle: const Text('Swipe down on pages to reload them'), + secondary: const Icon(MdiIcons.gestureSwipeDown), + value: pullToRefreshEnabled, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.pullToRefreshEnabled(value), + ); + }, + ); + } +} + +class _DoubleBackCloseTabTile extends HookConsumerWidget { + const _DoubleBackCloseTabTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final doubleBackCloseTab = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.doubleBackCloseTab), + ); + + return SwitchListTile.adaptive( + title: const Text('Double Back to Close Tab'), + subtitle: const Text( + 'When enabled, press back twice to close the tab. When disabled, back button only navigates page history.', + ), + secondary: const Icon(MdiIcons.gestureDoubleTap), + value: doubleBackCloseTab, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.doubleBackCloseTab(value), + ); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart b/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart new file mode 100644 index 00000000..b0cadea9 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; + +class FingerprintingSettingsScreen extends StatelessWidget { + const FingerprintingSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Fingerprinting')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _MainProtectionSection(), + _IdentitySection(), + _AdvancedSection(), + ], + ); + }, + ), + ), + ); + } +} + +class _MainProtectionSection extends StatelessWidget { + const _MainProtectionSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Main Protection'), + _FingerprintProtectionTile(), + ], + ); + } +} + +class _IdentitySection extends StatelessWidget { + const _IdentitySection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Identity'), + _BrowserLanguagesTile(), + ], + ); + } +} + +class _AdvancedSection extends StatelessWidget { + const _AdvancedSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Advanced'), + _ResistFingerprintingTile(), + ], + ); + } +} + +class _BrowserLanguagesTile extends StatelessWidget { + const _BrowserLanguagesTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Browser Languages'), + subtitle: const Text('Configure language preferences'), + 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 _FingerprintProtectionTile extends StatelessWidget { + const _FingerprintProtectionTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Fingerprint Protection'), + subtitle: const Text( + 'Granular control over browser fingerprinting', + ), + 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 _ResistFingerprintingTile extends StatelessWidget { + const _ResistFingerprintingTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Resist Fingerprinting'), + subtitle: const Text( + 'Advanced fingerprinting protection 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 const WebEngineHardeningGroupRoute( + group: 'Resist Fingerprinting', + ).push(context); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart deleted file mode 100644 index bb8eba10..00000000 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ /dev/null @@ -1,949 +0,0 @@ -/* - * Copyright (c) 2024-2025 Fabian Freund. - * - * This file is part of WebLibre - * (see https://weblibre.eu). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import 'package:fading_scroll/fading_scroll.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:nullability/nullability.dart'; -import 'package:weblibre/core/design/app_colors.dart'; -import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; -import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; -import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart'; -import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; -import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart'; -import 'package:weblibre/features/user/data/models/general_settings.dart'; -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 StatelessWidget { - const GeneralSettingsScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('General Settings')), - body: FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return ListView( - controller: controller, - children: const [ - _ThemeSection(), - _NewTabDefaultSection(), - _ExternalLinkHandlingSection(), - _DefaultSearchProviderSection(), - _AutocompleteProviderSection(), - _OnDeviceAiTile(), - _EnableReaderModeTile(), - _EnforceReaderModeTile(), - _ReaderModeInTabBarTile(), - _TabBarPositionSection(), - _ShowContextualTabBarTile(), - _ShowQuickTabSwitcherBarTile(), - _QuickTabSwitcherModeSection(), - _CreateChildTabsTile(), - _ShowExtensionShortcutTile(), - _AutoHideTabBarTile(), - _BottomSheetTabViewTile(), - _TabBarSwipeBehaviorSection(), - _PullToRefreshTile(), - _DoubleBackCloseTabTile(), - _IconCacheTile(), - ], - ); - }, - ), - ); - } -} - -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 appColors = AppColors.of(context); - 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 appColors = AppColors.of(context); - 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 _PullToRefreshTile extends HookConsumerWidget { - const _PullToRefreshTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pullToRefreshEnabled = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled), - ); - - return SwitchListTile.adaptive( - title: const Text('Pull to Refresh'), - subtitle: const Text('Swipe down on pages to reload them'), - secondary: const Icon(MdiIcons.gestureSwipeDown), - value: pullToRefreshEnabled, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.pullToRefreshEnabled(value), - ); - }, - ); - } -} - -class _DoubleBackCloseTabTile extends HookConsumerWidget { - const _DoubleBackCloseTabTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final doubleBackCloseTab = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.doubleBackCloseTab), - ); - - return SwitchListTile.adaptive( - title: const Text('Double Back to Close Tab'), - subtitle: const Text( - 'When enabled, press back twice to close the tab. When disabled, back button only navigates page history.', - ), - secondary: const Icon(MdiIcons.gestureDoubleTap), - value: doubleBackCloseTab, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.doubleBackCloseTab(value), - ); - }, - ); - } -} - -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/privacy_security_settings.dart similarity index 88% rename from app/lib/features/settings/presentation/screens/web_engine_settings.dart rename to app/lib/features/settings/presentation/screens/privacy_security_settings.dart index 6bd3f4f3..b444a006 100644 --- a/app/lib/features/settings/presentation/screens/web_engine_settings.dart +++ b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart @@ -26,41 +26,112 @@ import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; import 'package:weblibre/features/user/data/models/engine_settings.dart'; 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 StatelessWidget { - const WebEngineSettingsScreen(); +class PrivacySecuritySettingsScreen extends StatelessWidget { + const PrivacySecuritySettingsScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Web Engine Settings')), - body: FadingScroll( - fadingSize: 25, - builder: (context, controller) { + appBar: AppBar(title: const Text('Privacy & Security')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { return ListView( controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _IncognitoModeSection(), - _DeleteBrowsingDataTile(), - _AutoClearHistorySection(), - _BrowserLanguagesTile(), - _GlobalPrivacyControlTile(), - _HttpsOnlyModeSection(), - _DnsTile(), + _PrivacyModesSection(), _TrackingProtectionSection(), - _BounceTrackingProtectionTile(), - _QueryParameterStrippingSection(), - _PdfViewerTile(), - _WebEngineHardeningTile(), + _ConnectionSecuritySection(), + _DataManagementSection(), + _AdvancedSection(), ], ); }, ), + ), + ); + } +} + +class _PrivacyModesSection extends StatelessWidget { + const _PrivacyModesSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Privacy Modes'), + _IncognitoModeSection(), + _GlobalPrivacyControlTile(), + ], + ); + } +} + +class _TrackingProtectionSection extends StatelessWidget { + const _TrackingProtectionSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Tracking Protection'), + _EnhancedTrackingProtectionSection(), + _BounceTrackingProtectionTile(), + _QueryParameterStrippingSection(), + ], + ); + } +} + +class _ConnectionSecuritySection extends StatelessWidget { + const _ConnectionSecuritySection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Connection Security'), + _HttpsOnlyModeSection(), + _DnsTile(), + ], + ); + } +} + +class _DataManagementSection extends StatelessWidget { + const _DataManagementSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Data Management'), + _DeleteBrowsingDataTile(), + _AutoClearHistorySection(), + ], + ); + } +} + +class _AdvancedSection extends StatelessWidget { + const _AdvancedSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Advanced'), + _WebEngineHardeningTile(), + ], ); } } @@ -270,26 +341,6 @@ class _AutoClearHistorySection extends HookConsumerWidget { } } -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(); @@ -395,8 +446,8 @@ class _DnsTile extends StatelessWidget { } } -class _TrackingProtectionSection extends HookConsumerWidget { - const _TrackingProtectionSection(); +class _EnhancedTrackingProtectionSection extends HookConsumerWidget { + const _EnhancedTrackingProtectionSection(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -558,8 +609,8 @@ class _QueryParameterStrippingSection extends HookConsumerWidget { saveEngineSettingsControllerProvider.notifier, ) .save( - (currentSettings) => currentSettings.copyWith - .queryParameterStripping(value.first), + (currentSettings) => + currentSettings.copyWith.queryParameterStripping(value.first), ); }, ), @@ -570,33 +621,6 @@ class _QueryParameterStrippingSection extends HookConsumerWidget { } } -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(); diff --git a/app/lib/features/settings/presentation/screens/search_content_settings.dart b/app/lib/features/settings/presentation/screens/search_content_settings.dart new file mode 100644 index 00000000..1a791bd8 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/search_content_settings.dart @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:nullability/nullability.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; +import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart'; +import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; +import 'package:weblibre/features/user/data/models/engine_settings.dart'; +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 SearchContentSettingsScreen extends StatelessWidget { + const SearchContentSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Search & Content')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _SearchSection(), + _ContentViewingSection(), + _ContentEnhancementSection(), + _ExtensionsSection(), + ], + ); + }, + ), + ), + ); + } +} + +class _SearchSection extends StatelessWidget { + const _SearchSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Search'), + _DefaultSearchProviderSection(), + _BangsTile(), + _AutocompleteProviderSection(), + ], + ); + } +} + +class _ContentViewingSection extends StatelessWidget { + const _ContentViewingSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Content Viewing'), + _PdfViewerTile(), + _EnableReaderModeTile(), + _ReaderModeInTabBarTile(), + _EnforceReaderModeTile(), + ], + ); + } +} + +class _ContentEnhancementSection extends StatelessWidget { + const _ContentEnhancementSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Content Enhancement'), + _OnDeviceAiTile(), + ], + ); + } +} + +class _ExtensionsSection extends StatelessWidget { + const _ExtensionsSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Extensions'), + _AddonCollectionTile(), + ], + ); + } +} + +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 _BangsTile extends StatelessWidget { + const _BangsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Bangs'), + subtitle: const Text('Search shortcuts management'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.exclamationThick), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await BangSettingsRoute().push(context); + }, + ); + } +} + +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 _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 _AddonCollectionTile extends StatelessWidget { + const _AddonCollectionTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Custom Extension Collection'), + subtitle: const Text( + 'Custom Add-on Collections are curated lists of extensions that users can create and share.', + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.puzzle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await AddonCollectionRoute().push(context); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index b2eac501..9904dc68 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -10,7 +10,7 @@ * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of + * but WITHOUT ANY WARRANTY; without even the the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * @@ -36,74 +36,13 @@ class SettingsScreen extends HookConsumerWidget { return ListView( controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), - children: [ - Card( - color: Theme.of(context).highlightColor, - clipBehavior: Clip.antiAlias, - child: ListTile( - title: const Text('General'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(Icons.settings), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await GeneralSettingsRoute().push(context); - }, - ), - ), - const SizedBox(height: 8), - Card( - color: Theme.of(context).highlightColor, - clipBehavior: Clip.antiAlias, - child: ListTile( - title: const Text('Web Engine'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.web), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await WebEngineSettingsRoute().push(context); - }, - ), - ), - const SizedBox(height: 8), - Card( - color: Theme.of(context).highlightColor, - clipBehavior: Clip.antiAlias, - child: ListTile( - title: const Text('Bangs'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(MdiIcons.exclamationThick), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await BangSettingsRoute().push(context); - }, - ), - ), - const SizedBox(height: 8), - Card( - color: Theme.of(context).highlightColor, - clipBehavior: Clip.antiAlias, - child: ListTile( - title: const Text('Developer'), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - leading: const Icon(Icons.developer_mode), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await DeveloperSettingsRoute().push(context); - }, - ), - ), + children: const [ + _AppearanceDisplayTile(), + _PrivacySecurityTile(), + _SearchContentTile(), + _TabsBehaviorTile(), + _FingerprintingTile(), + _AdvancedTile(), ], ); }, @@ -111,3 +50,153 @@ class SettingsScreen extends HookConsumerWidget { ); } } + +class _AppearanceDisplayTile extends StatelessWidget { + const _AppearanceDisplayTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Appearance & Display'), + subtitle: const Text('Theme, tabs, toolbars, gestures'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(Icons.palette), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await AppearanceDisplaySettingsRoute().push(context); + }, + ), + ); + } +} + +class _PrivacySecurityTile extends StatelessWidget { + const _PrivacySecurityTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Privacy & Security'), + subtitle: const Text('Tracking protection, data clearing'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.shieldLock), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await PrivacySecuritySettingsRoute().push(context); + }, + ), + ); + } +} + +class _SearchContentTile extends StatelessWidget { + const _SearchContentTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Search & Content'), + subtitle: const Text('Search providers, reader mode, AI'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.magnify), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await SearchContentSettingsRoute().push(context); + }, + ), + ); + } +} + +class _TabsBehaviorTile extends StatelessWidget { + const _TabsBehaviorTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Tabs & Behavior'), + subtitle: const Text('New tab defaults, link handling'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.tab), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await TabsBehaviorSettingsRoute().push(context); + }, + ), + ); + } +} + +class _FingerprintingTile extends StatelessWidget { + const _FingerprintingTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Fingerprinting'), + subtitle: const Text('Language, 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 FingerprintingSettingsRoute().push(context); + }, + ), + ); + } +} + +class _AdvancedTile extends StatelessWidget { + const _AdvancedTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Advanced'), + subtitle: const Text('JavaScript, user agent, debugging'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(Icons.developer_mode), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await AdvancedSettingsRoute().push(context); + }, + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart b/app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart new file mode 100644 index 00000000..bd13671e --- /dev/null +++ b/app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/design/app_colors.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; + +class TabsBehaviorSettingsScreen extends StatelessWidget { + const TabsBehaviorSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Tabs & Behavior')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + _TabCreationSection(), + _TabOrganizationSection(), + _TabInteractionSection(), + ], + ); + }, + ), + ), + ); + } +} + +class _TabCreationSection extends StatelessWidget { + const _TabCreationSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Tab Creation'), + _NewTabDefaultSection(), + _ExternalLinkHandlingSection(), + ], + ); + } +} + +class _TabOrganizationSection extends StatelessWidget { + const _TabOrganizationSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Tab Organization'), + _CreateChildTabsTile(), + ], + ); + } +} + +class _TabInteractionSection extends StatelessWidget { + const _TabInteractionSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Tab Interaction'), + _TabBarSwipeBehaviorSection(), + _ShowExtensionShortcutTile(), + ], + ); + } +} + +class _NewTabDefaultSection extends HookConsumerWidget { + const _NewTabDefaultSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appColors = AppColors.of(context); + 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 appColors = AppColors.of(context); + 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 _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 _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', + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/widgets/sections.dart b/app/lib/features/settings/presentation/widgets/sections.dart index ebfce631..16ccefdc 100644 --- a/app/lib/features/settings/presentation/widgets/sections.dart +++ b/app/lib/features/settings/presentation/widgets/sections.dart @@ -27,7 +27,7 @@ class SettingSection extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,