From 3664dca11f6456da00a5f3c6a0cc78b6022bd9fb Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 13 Mar 2026 06:21:15 +0100 Subject: [PATCH] reorganize settings --- app/lib/core/routing/routes.dart | 12 +- app/lib/core/routing/routes.g.dart | 122 +++- app/lib/core/routing/routes.settings.dart | 80 ++- .../screens/advanced_settings.dart | 183 +---- .../screens/appearance_display_settings.dart | 658 ------------------ ...r_settings.dart => browsing_settings.dart} | 155 +++-- .../screens/experimental_settings.dart | 129 ++++ .../screens/extensions_settings.dart | 107 +++ .../screens/fingerprinting_settings.dart | 158 ----- .../screens/general_settings.dart | 295 ++++++++ .../screens/privacy_security_settings.dart | 142 ++-- ...ent_settings.dart => search_settings.dart} | 240 ++----- .../presentation/screens/settings.dart | 144 ++-- ...ings.dart => toolbar_layout_settings.dart} | 85 ++- .../screens/web_content_settings.dart | 410 +++++++++++ 15 files changed, 1510 insertions(+), 1410 deletions(-) delete mode 100644 app/lib/features/settings/presentation/screens/appearance_display_settings.dart rename app/lib/features/settings/presentation/screens/{tabs_behavior_settings.dart => browsing_settings.dart} (85%) create mode 100644 app/lib/features/settings/presentation/screens/experimental_settings.dart create mode 100644 app/lib/features/settings/presentation/screens/extensions_settings.dart delete mode 100644 app/lib/features/settings/presentation/screens/fingerprinting_settings.dart create mode 100644 app/lib/features/settings/presentation/screens/general_settings.dart rename app/lib/features/settings/presentation/screens/{search_content_settings.dart => search_settings.dart} (63%) rename app/lib/features/settings/presentation/screens/{tab_bar_settings.dart => toolbar_layout_settings.dart} (90%) create mode 100644 app/lib/features/settings/presentation/screens/web_content_settings.dart diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 403d50d4..fa4576ca 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -58,21 +58,23 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/c 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/browsing_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/contextual_toolbar_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.dart'; import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.dart'; +import 'package:weblibre/features/settings/presentation/screens/experimental_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/extensions_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart'; -import 'package:weblibre/features/settings/presentation/screens/fingerprinting_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/general_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/search_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/settings.dart'; -import 'package:weblibre/features/settings/presentation/screens/tab_bar_settings.dart'; -import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart'; +import 'package:weblibre/features/settings/presentation/screens/web_content_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/sync/presentation/screens/sync_settings.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index fa87aea3..8dbf6502 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -1375,14 +1375,14 @@ RouteBase get $settingsRoute => GoRouteData.$route( factory: $SettingsRoute._fromState, routes: [ GoRouteData.$route( - path: 'appearance_display', - name: 'AppearanceDisplaySettingsRoute', - factory: $AppearanceDisplaySettingsRoute._fromState, + path: 'general', + name: 'GeneralSettingsRoute', + factory: $GeneralSettingsRoute._fromState, ), GoRouteData.$route( - path: 'tab_bar', - name: 'TabBarSettingsRoute', - factory: $TabBarSettingsRoute._fromState, + path: 'browsing', + name: 'BrowsingSettingsRoute', + factory: $BrowsingSettingsRoute._fromState, ), GoRouteData.$route( path: 'privacy_security', @@ -1390,25 +1390,35 @@ RouteBase get $settingsRoute => GoRouteData.$route( factory: $PrivacySecuritySettingsRoute._fromState, ), GoRouteData.$route( - path: 'search_content', - name: 'SearchContentSettingsRoute', - factory: $SearchContentSettingsRoute._fromState, + path: 'toolbar_layout', + name: 'ToolbarLayoutSettingsRoute', + factory: $ToolbarLayoutSettingsRoute._fromState, ), GoRouteData.$route( - path: 'tabs_behavior', - name: 'TabsBehaviorSettingsRoute', - factory: $TabsBehaviorSettingsRoute._fromState, + path: 'web_content', + name: 'WebContentSettingsRoute', + factory: $WebContentSettingsRoute._fromState, ), GoRouteData.$route( - path: 'fingerprinting', - name: 'FingerprintingSettingsRoute', - factory: $FingerprintingSettingsRoute._fromState, + path: 'search', + name: 'SearchSettingsRoute', + factory: $SearchSettingsRoute._fromState, + ), + GoRouteData.$route( + path: 'extensions', + name: 'ExtensionsSettingsRoute', + factory: $ExtensionsSettingsRoute._fromState, ), GoRouteData.$route( path: 'advanced', name: 'AdvancedSettingsRoute', factory: $AdvancedSettingsRoute._fromState, ), + GoRouteData.$route( + path: 'experimental', + name: 'ExperimentalSettingsRoute', + factory: $ExperimentalSettingsRoute._fromState, + ), GoRouteData.$route( path: 'bang', name: 'BangSettingsRoute', @@ -1504,12 +1514,12 @@ mixin $SettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $AppearanceDisplaySettingsRoute on GoRouteData { - static AppearanceDisplaySettingsRoute _fromState(GoRouterState state) => - AppearanceDisplaySettingsRoute(); +mixin $GeneralSettingsRoute on GoRouteData { + static GeneralSettingsRoute _fromState(GoRouterState state) => + GeneralSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/appearance_display'); + String get location => GoRouteData.$location('/settings/general'); @override void go(BuildContext context) => context.go(location); @@ -1525,12 +1535,12 @@ mixin $AppearanceDisplaySettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $TabBarSettingsRoute on GoRouteData { - static TabBarSettingsRoute _fromState(GoRouterState state) => - TabBarSettingsRoute(); +mixin $BrowsingSettingsRoute on GoRouteData { + static BrowsingSettingsRoute _fromState(GoRouterState state) => + BrowsingSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/tab_bar'); + String get location => GoRouteData.$location('/settings/browsing'); @override void go(BuildContext context) => context.go(location); @@ -1567,12 +1577,12 @@ mixin $PrivacySecuritySettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $SearchContentSettingsRoute on GoRouteData { - static SearchContentSettingsRoute _fromState(GoRouterState state) => - SearchContentSettingsRoute(); +mixin $ToolbarLayoutSettingsRoute on GoRouteData { + static ToolbarLayoutSettingsRoute _fromState(GoRouterState state) => + ToolbarLayoutSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/search_content'); + String get location => GoRouteData.$location('/settings/toolbar_layout'); @override void go(BuildContext context) => context.go(location); @@ -1588,12 +1598,12 @@ mixin $SearchContentSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $TabsBehaviorSettingsRoute on GoRouteData { - static TabsBehaviorSettingsRoute _fromState(GoRouterState state) => - TabsBehaviorSettingsRoute(); +mixin $WebContentSettingsRoute on GoRouteData { + static WebContentSettingsRoute _fromState(GoRouterState state) => + WebContentSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/tabs_behavior'); + String get location => GoRouteData.$location('/settings/web_content'); @override void go(BuildContext context) => context.go(location); @@ -1609,12 +1619,33 @@ mixin $TabsBehaviorSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } -mixin $FingerprintingSettingsRoute on GoRouteData { - static FingerprintingSettingsRoute _fromState(GoRouterState state) => - FingerprintingSettingsRoute(); +mixin $SearchSettingsRoute on GoRouteData { + static SearchSettingsRoute _fromState(GoRouterState state) => + SearchSettingsRoute(); @override - String get location => GoRouteData.$location('/settings/fingerprinting'); + String get location => GoRouteData.$location('/settings/search'); + + @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 $ExtensionsSettingsRoute on GoRouteData { + static ExtensionsSettingsRoute _fromState(GoRouterState state) => + ExtensionsSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/extensions'); @override void go(BuildContext context) => context.go(location); @@ -1651,6 +1682,27 @@ mixin $AdvancedSettingsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $ExperimentalSettingsRoute on GoRouteData { + static ExperimentalSettingsRoute _fromState(GoRouterState state) => + ExperimentalSettingsRoute(); + + @override + String get location => GoRouteData.$location('/settings/experimental'); + + @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 $BangSettingsRoute on GoRouteData { static BangSettingsRoute _fromState(GoRouterState state) => BangSettingsRoute(); diff --git a/app/lib/core/routing/routes.settings.dart b/app/lib/core/routing/routes.settings.dart index 13d1e445..8c7eb7b7 100644 --- a/app/lib/core/routing/routes.settings.dart +++ b/app/lib/core/routing/routes.settings.dart @@ -23,34 +23,42 @@ part of 'routes.dart'; name: 'SettingsRoute', path: '/settings', routes: [ - TypedGoRoute( - name: 'AppearanceDisplaySettingsRoute', - path: 'appearance_display', + TypedGoRoute( + name: 'GeneralSettingsRoute', + path: 'general', ), - TypedGoRoute( - name: 'TabBarSettingsRoute', - path: 'tab_bar', + TypedGoRoute( + name: 'BrowsingSettingsRoute', + path: 'browsing', ), TypedGoRoute( name: 'PrivacySecuritySettingsRoute', path: 'privacy_security', ), - TypedGoRoute( - name: 'SearchContentSettingsRoute', - path: 'search_content', + TypedGoRoute( + name: 'ToolbarLayoutSettingsRoute', + path: 'toolbar_layout', ), - TypedGoRoute( - name: 'TabsBehaviorSettingsRoute', - path: 'tabs_behavior', + TypedGoRoute( + name: 'WebContentSettingsRoute', + path: 'web_content', ), - TypedGoRoute( - name: 'FingerprintingSettingsRoute', - path: 'fingerprinting', + TypedGoRoute( + name: 'SearchSettingsRoute', + path: 'search', + ), + TypedGoRoute( + name: 'ExtensionsSettingsRoute', + path: 'extensions', ), TypedGoRoute( name: 'AdvancedSettingsRoute', path: 'advanced', ), + TypedGoRoute( + name: 'ExperimentalSettingsRoute', + path: 'experimental', + ), TypedGoRoute(name: 'BangSettingsRoute', path: 'bang'), TypedGoRoute( name: 'WebEngineHardeningRoute', @@ -106,18 +114,17 @@ class SettingsRoute extends GoRouteData with $SettingsRoute { } } -class AppearanceDisplaySettingsRoute extends GoRouteData - with $AppearanceDisplaySettingsRoute { +class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const AppearanceDisplaySettingsScreen(); + return const GeneralSettingsScreen(); } } -class TabBarSettingsRoute extends GoRouteData with $TabBarSettingsRoute { +class BrowsingSettingsRoute extends GoRouteData with $BrowsingSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const TabBarSettingsScreen(); + return const BrowsingSettingsScreen(); } } @@ -129,27 +136,34 @@ class PrivacySecuritySettingsRoute extends GoRouteData } } -class SearchContentSettingsRoute extends GoRouteData - with $SearchContentSettingsRoute { +class ToolbarLayoutSettingsRoute extends GoRouteData + with $ToolbarLayoutSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const SearchContentSettingsScreen(); + return const ToolbarLayoutSettingsScreen(); } } -class TabsBehaviorSettingsRoute extends GoRouteData - with $TabsBehaviorSettingsRoute { +class WebContentSettingsRoute extends GoRouteData + with $WebContentSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const TabsBehaviorSettingsScreen(); + return const WebContentSettingsScreen(); } } -class FingerprintingSettingsRoute extends GoRouteData - with $FingerprintingSettingsRoute { +class SearchSettingsRoute extends GoRouteData with $SearchSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { - return const FingerprintingSettingsScreen(); + return const SearchSettingsScreen(); + } +} + +class ExtensionsSettingsRoute extends GoRouteData + with $ExtensionsSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const ExtensionsSettingsScreen(); } } @@ -160,6 +174,14 @@ class AdvancedSettingsRoute extends GoRouteData with $AdvancedSettingsRoute { } } +class ExperimentalSettingsRoute extends GoRouteData + with $ExperimentalSettingsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const ExperimentalSettingsScreen(); + } +} + class BangSettingsRoute extends GoRouteData with $BangSettingsRoute { @override Widget build(BuildContext context, GoRouterState state) { diff --git a/app/lib/features/settings/presentation/screens/advanced_settings.dart b/app/lib/features/settings/presentation/screens/advanced_settings.dart index cc8bd479..f1dfeb93 100644 --- a/app/lib/features/settings/presentation/screens/advanced_settings.dart +++ b/app/lib/features/settings/presentation/screens/advanced_settings.dart @@ -29,13 +29,11 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/providers/app_state.dart'; import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.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/presentation/dialogs/quit_browser_dialog.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'; @@ -57,11 +55,9 @@ class AdvancedSettingsScreen extends StatelessWidget { controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _ContentBehaviorSection(), + _ContentIdentitySection(), _ExperimentalSection(), - _ExtensionsSection(), - _StorageDebuggingSection(), - _ResetSection(), + _DeveloperToolsSection(), ], ); }, @@ -71,14 +67,14 @@ class AdvancedSettingsScreen extends StatelessWidget { } } -class _ContentBehaviorSection extends StatelessWidget { - const _ContentBehaviorSection(); +class _ContentIdentitySection extends StatelessWidget { + const _ContentIdentitySection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Content Behavior'), + SettingSection(name: 'Content & Identity'), _JavaScriptTile(), _UserAgentTile(), _EnterpriseRootsTile(), @@ -87,21 +83,6 @@ class _ContentBehaviorSection extends StatelessWidget { } } -class _ExtensionsSection extends StatelessWidget { - const _ExtensionsSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Extensions'), - _InstallLocalAddonTile(), - _AddonCollectionTile(), - ], - ); - } -} - class _ExperimentalSection extends StatelessWidget { const _ExperimentalSection(); @@ -110,91 +91,23 @@ class _ExperimentalSection extends StatelessWidget { return const Column( children: [ SettingSection(name: 'Experimental'), - _IsolatedProcessEnabledTile(), - _AppZygoteProcessEnabledTile(), + _ExperimentalSettingsTile(), ], ); } } -class _InstallLocalAddonTile extends StatelessWidget { - const _InstallLocalAddonTile(); - - @override - Widget build(BuildContext context) { - return CustomListTile( - title: 'Install from File', - subtitle: 'Install an extension from a local .xpi file', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - MdiIcons.puzzle, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await showInstallLocalAddonDialog(context); - }, - icon: const Icon(Icons.file_open), - label: const Text('Install'), - ), - ); - } -} - -class _AddonCollectionTile extends StatelessWidget { - const _AddonCollectionTile(); - - @override - Widget build(BuildContext context) { - return CustomListTile( - title: 'Custom Collection', - subtitle: 'Use a custom Mozilla addon collection', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - MdiIcons.folderMultiple, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await AddonCollectionRoute().push(context); - }, - icon: const Icon(Icons.settings), - label: const Text('Configure'), - ), - ); - } -} - -class _StorageDebuggingSection extends StatelessWidget { - const _StorageDebuggingSection(); +class _DeveloperToolsSection extends StatelessWidget { + const _DeveloperToolsSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Storage & Debugging'), + SettingSection(name: 'Developer Tools'), _IconCacheTile(), _ErrorLogsTile(), _DartVmTile(), - ], - ); - } -} - -class _ResetSection extends StatelessWidget { - const _ResetSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Reset'), _ResetUITile(), ], ); @@ -303,67 +216,22 @@ class _EnterpriseRootsTile extends HookConsumerWidget { } } -class _IsolatedProcessEnabledTile extends HookConsumerWidget { - const _IsolatedProcessEnabledTile(); +class _ExperimentalSettingsTile extends StatelessWidget { + const _ExperimentalSettingsTile(); @override - Widget build(BuildContext context, WidgetRef ref) { - final isolatedProcessEnabled = ref.watch( - engineSettingsWithDefaultsProvider.select( - (s) => s.isolatedProcessEnabled, + Widget build(BuildContext context) { + return ListTile( + title: const Text('Experimental Features'), + subtitle: const Text('Low-level runtime features and startup behavior'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, ), - ); - - return SwitchListTile.adaptive( - title: const Text('Isolated Content Process'), - subtitle: const Text( - 'Run web content in an isolated process. Requires app restart.', - ), - secondary: const Icon(MdiIcons.shieldCheck), - value: isolatedProcessEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.isolatedProcessEnabled(value), - ); - if (context.mounted) { - await _showRestartDialog(context, ref); - } - }, - ); - } -} - -class _AppZygoteProcessEnabledTile extends HookConsumerWidget { - const _AppZygoteProcessEnabledTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appZygoteProcessEnabled = ref.watch( - engineSettingsWithDefaultsProvider.select( - (s) => s.appZygoteProcessEnabled, - ), - ); - - return SwitchListTile.adaptive( - title: const Text('App Zygote Process'), - subtitle: const Text( - 'Preload content service via App Zygote for faster isolated process startup. Requires Android 10+ and app restart.', - ), - secondary: const Icon(MdiIcons.rocketLaunch), - value: appZygoteProcessEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.appZygoteProcessEnabled(value), - ); - if (context.mounted) { - await _showRestartDialog(context, ref); - } + leading: const Icon(MdiIcons.flaskOutline), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await ExperimentalSettingsRoute().push(context); }, ); } @@ -511,10 +379,3 @@ class _ResetUITile extends ConsumerWidget { ); } } - -Future _showRestartDialog(BuildContext context, WidgetRef ref) async { - final result = await showQuitBrowserDialog(context); - if (result == true && context.mounted) { - await exitApp(ProviderScope.containerOf(context)); - } -} diff --git a/app/lib/features/settings/presentation/screens/appearance_display_settings.dart b/app/lib/features/settings/presentation/screens/appearance_display_settings.dart deleted file mode 100644 index 5b9fa831..00000000 --- a/app/lib/features/settings/presentation/screens/appearance_display_settings.dart +++ /dev/null @@ -1,658 +0,0 @@ -/* - * Copyright (c) 2024-2026 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_hooks/flutter_hooks.dart'; -import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.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/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 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(), - _WebContentSection(), - _TabBarSection(), - _TabViewSection(), - _GesturesSection(), - ], - ); - }, - ), - ), - ); - } -} - -class _VisualSection extends StatelessWidget { - const _VisualSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Visual'), - _UiZoomSection(), - _DisableAnimationsTile(), - _ShowModalBarrierTile(), - _ThemeSection(), - ], - ); - } -} - -class _UiZoomSection extends HookConsumerWidget { - const _UiZoomSection(); - - static final _sliderDivisions = - ((maxUiScaleFactor - minUiScaleFactor) / uiScaleFactorStep).round(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final uiScaleFactor = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.uiScaleFactor), - ); - final sliderValue = useState(uiScaleFactor); - - useEffect(() { - sliderValue.value = uiScaleFactor; - return null; - }, [uiScaleFactor]); - - final sliderLabel = '${(sliderValue.value * 100).round()}%'; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ListTile( - title: Text('User Interface Zoom'), - subtitle: Text('Make the user interface smaller or larger'), - leading: Icon(Icons.zoom_in), - contentPadding: EdgeInsets.zero, - ), - Row( - children: [ - Text(sliderLabel, style: Theme.of(context).textTheme.titleLarge), - Expanded( - child: Slider( - min: minUiScaleFactor, - max: maxUiScaleFactor, - divisions: _sliderDivisions, - label: sliderLabel, - value: sliderValue.value.clamp( - minUiScaleFactor, - maxUiScaleFactor, - ), - onChanged: (value) { - sliderValue.value = value; - }, - onChangeEnd: (value) async { - final normalized = _normalizeUiScale(value); - sliderValue.value = normalized; - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .uiScaleFactor(normalized), - ); - }, - ), - ), - ], - ), - ], - ), - ); - } -} - -double _normalizeUiScale(double value) { - final clampedValue = value.clamp(minUiScaleFactor, maxUiScaleFactor); - final stepIndex = ((clampedValue - minUiScaleFactor) / uiScaleFactorStep) - .round(); - final normalized = minUiScaleFactor + (stepIndex * uiScaleFactorStep); - return normalized.clamp(minUiScaleFactor, maxUiScaleFactor); -} - -class _DisableAnimationsTile extends HookConsumerWidget { - const _DisableAnimationsTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final disableAnimations = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.disableAnimations), - ); - - return SwitchListTile.adaptive( - title: const Text('Disable Animations'), - subtitle: const Text('Reduce motion and turn off app animations'), - secondary: const Icon(Icons.animation), - value: disableAnimations, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.disableAnimations(value), - ); - }, - ); - } -} - -class _ShowModalBarrierTile extends HookConsumerWidget { - const _ShowModalBarrierTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final showModalBarrier = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.showModalBarrier), - ); - - return SwitchListTile.adaptive( - title: const Text('Show Modal Barrier'), - subtitle: const Text( - 'Dim the background behind dialogs and bottom sheets', - ), - secondary: const Icon(Icons.layers), - value: showModalBarrier, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.showModalBarrier(value), - ); - }, - ); - } -} - -class _TabBarSection extends StatelessWidget { - const _TabBarSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Tab Bar'), - _TabBarSettingsTile(), - ], - ); - } -} - -class _TabViewSection extends StatelessWidget { - const _TabViewSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Tab View'), - _BottomSheetTabViewTile(), - _TabListShowFaviconsTile(), - ], - ); - } -} - -class _TabBarSettingsTile extends StatelessWidget { - const _TabBarSettingsTile(); - - @override - Widget build(BuildContext context) { - return ListTile( - title: const Text('Customize Tab Bar'), - subtitle: const Text( - 'Layout, contextual bar, quick switcher, and preview', - ), - leading: const Icon(MdiIcons.tabUnselected), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await TabBarSettingsRoute().push(context); - }, - ); - } -} - -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 _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 _TabListShowFaviconsTile extends HookConsumerWidget { - const _TabListShowFaviconsTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final tabListShowFavicons = ref.watch( - generalSettingsWithDefaultsProvider.select((s) => s.tabListShowFavicons), - ); - - return SwitchListTile.adaptive( - title: const Text('Show Favicons in List View'), - subtitle: const Text( - 'Display website icons instead of page thumbnails in tab list view', - ), - secondary: const Icon(MdiIcons.web), - value: tabListShowFavicons, - onChanged: (value) async { - await ref - .read(saveGeneralSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.tabListShowFavicons(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), - ); - }, - ); - } -} - -class _WebContentSection extends StatelessWidget { - const _WebContentSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Web Content'), - _WebFontsEnabledTile(), - _AutomaticFontSizeAdjustmentTile(), - _FontSizeFactorSlider(), - _FontInflationTile(), - _InputAutoZoomEnabledTile(), - ], - ); - } -} - -class _WebFontsEnabledTile extends HookConsumerWidget { - const _WebFontsEnabledTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final webFontsEnabled = ref.watch( - engineSettingsWithDefaultsProvider.select((s) => s.webFontsEnabled), - ); - - return SwitchListTile.adaptive( - title: const Text('Web Fonts'), - subtitle: const Text('Allow websites to use custom fonts'), - secondary: const Icon(MdiIcons.formatFont), - value: webFontsEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.webFontsEnabled(value), - ); - }, - ); - } -} - -class _AutomaticFontSizeAdjustmentTile extends HookConsumerWidget { - const _AutomaticFontSizeAdjustmentTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final automaticFontSizeAdjustment = ref.watch( - engineSettingsWithDefaultsProvider.select( - (s) => s.automaticFontSizeAdjustment, - ), - ); - - return SwitchListTile.adaptive( - title: const Text('Automatic Font Size'), - subtitle: const Text( - 'Automatically adjust font size based on system settings. Disable to manually control font size factor and inflation.', - ), - secondary: const Icon(MdiIcons.formatFontSizeIncrease), - value: automaticFontSizeAdjustment, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.automaticFontSizeAdjustment(value), - ); - }, - ); - } -} - -class _FontSizeFactorSlider extends HookConsumerWidget { - const _FontSizeFactorSlider(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final automaticFontSizeAdjustment = ref.watch( - engineSettingsWithDefaultsProvider.select( - (s) => s.automaticFontSizeAdjustment, - ), - ); - final fontSizeFactor = ref.watch( - engineSettingsWithDefaultsProvider.select((s) => s.fontSizeFactor), - ); - final sliderValue = useState(fontSizeFactor); - - useEffect(() { - sliderValue.value = fontSizeFactor; - return null; - }, [fontSizeFactor]); - - final sliderLabel = '${(sliderValue.value * 100).round()}%'; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - title: const Text('Font Size Factor'), - subtitle: Text( - automaticFontSizeAdjustment - ? 'Disabled while automatic font size is enabled' - : 'Scale web page text size', - ), - leading: const Icon(MdiIcons.formatSize), - contentPadding: EdgeInsets.zero, - enabled: !automaticFontSizeAdjustment, - ), - Row( - children: [ - Text( - sliderLabel, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: automaticFontSizeAdjustment - ? Theme.of(context).disabledColor - : null, - ), - ), - Expanded( - child: Slider( - min: 0.5, - max: 3.0, - divisions: 25, - label: sliderLabel, - value: sliderValue.value.clamp(0.5, 3.0), - onChanged: automaticFontSizeAdjustment - ? null - : (value) { - sliderValue.value = value; - }, - onChangeEnd: automaticFontSizeAdjustment - ? null - : (value) async { - final rounded = (value * 10).round() / 10; - sliderValue.value = rounded; - await ref - .read( - saveEngineSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => currentSettings.copyWith - .fontSizeFactor(rounded), - ); - }, - ), - ), - ], - ), - ], - ), - ); - } -} - -class _FontInflationTile extends HookConsumerWidget { - const _FontInflationTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final automaticFontSizeAdjustment = ref.watch( - engineSettingsWithDefaultsProvider.select( - (s) => s.automaticFontSizeAdjustment, - ), - ); - final fontInflationEnabled = ref.watch( - engineSettingsWithDefaultsProvider.select((s) => s.fontInflationEnabled), - ); - - return SwitchListTile.adaptive( - title: const Text('Font Inflation'), - subtitle: Text( - automaticFontSizeAdjustment - ? 'Disabled while automatic font size is enabled' - : 'Enlarge text on pages that lack a mobile viewport meta tag', - ), - secondary: const Icon(MdiIcons.formatTextVariantOutline), - value: fontInflationEnabled, - onChanged: automaticFontSizeAdjustment - ? null - : (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.fontInflationEnabled(value), - ); - }, - ); - } -} - -class _InputAutoZoomEnabledTile extends HookConsumerWidget { - const _InputAutoZoomEnabledTile(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final inputAutoZoomEnabled = ref.watch( - engineSettingsWithDefaultsProvider.select((s) => s.inputAutoZoomEnabled), - ); - - return SwitchListTile.adaptive( - title: const Text('Input Auto Zoom'), - subtitle: const Text('Automatically zoom in when focusing text inputs'), - secondary: const Icon(MdiIcons.formTextbox), - value: inputAutoZoomEnabled, - onChanged: (value) async { - await ref - .read(saveEngineSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.inputAutoZoomEnabled(value), - ); - }, - ); - } -} diff --git a/app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart b/app/lib/features/settings/presentation/screens/browsing_settings.dart similarity index 85% rename from app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart rename to app/lib/features/settings/presentation/screens/browsing_settings.dart index 4456942a..7d9198a4 100644 --- a/app/lib/features/settings/presentation/screens/tabs_behavior_settings.dart +++ b/app/lib/features/settings/presentation/screens/browsing_settings.dart @@ -31,13 +31,13 @@ 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}); +class BrowsingSettingsScreen extends StatelessWidget { + const BrowsingSettingsScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Tabs & Behavior')), + appBar: AppBar(title: const Text('Browsing')), body: SafeArea( child: FadingScroll( fadingSize: 25, @@ -46,10 +46,9 @@ class TabsBehaviorSettingsScreen extends StatelessWidget { controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _TabCreationSection(), - _TabOrganizationSection(), - _TabInteractionSection(), - _DownloadsSection(), + _TabsSection(), + _NavigationSection(), + _ExternalLinksSection(), ], ); }, @@ -59,31 +58,16 @@ class TabsBehaviorSettingsScreen extends StatelessWidget { } } -class _TabCreationSection extends StatelessWidget { - const _TabCreationSection(); +class _TabsSection extends StatelessWidget { + const _TabsSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Tab Creation'), + SettingSection(name: 'Tabs'), _NewTabDefaultSection(), _NewTabPositionSection(), - _ExternalLinkHandlingSection(), - _AppLinksModeSection(), - ], - ); - } -} - -class _TabOrganizationSection extends StatelessWidget { - const _TabOrganizationSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Tab Organization'), _ShowContainerUiTile(), _ShowIsolatedTabUiTile(), _CreateChildTabsTile(), @@ -92,15 +76,34 @@ class _TabOrganizationSection extends StatelessWidget { } } -class _TabInteractionSection extends StatelessWidget { - const _TabInteractionSection(); +class _NavigationSection extends StatelessWidget { + const _NavigationSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Tab Interaction'), + SettingSection(name: 'Navigation'), + _PullToRefreshTile(), + _DoubleBackCloseTabTile(), _TabBarSwipeBehaviorSection(), + _AppLinksModeSection(), + ], + ); + } +} + +class _ExternalLinksSection extends StatelessWidget { + const _ExternalLinksSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'External Links'), + _ExternalLinkHandlingSection(), + _UrlCleanerSettingsTile(), + _UnshortenerSettingsTile(), ], ); } @@ -528,44 +531,90 @@ class _AppLinksModeSection extends HookConsumerWidget { } } -class _DownloadsSection extends StatelessWidget { - const _DownloadsSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Downloads'), - _ExternalDownloadManagerTile(), - ], - ); - } -} - -class _ExternalDownloadManagerTile extends HookConsumerWidget { - const _ExternalDownloadManagerTile(); +class _PullToRefreshTile extends HookConsumerWidget { + const _PullToRefreshTile(); @override Widget build(BuildContext context, WidgetRef ref) { - final useExternalDownloadManager = ref.watch( - generalSettingsWithDefaultsProvider.select( - (s) => s.useExternalDownloadManager, - ), + final pullToRefreshEnabled = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled), ); return SwitchListTile.adaptive( - title: const Text('Use external download manager'), - subtitle: const Text('Manage downloads with another app'), - secondary: const Icon(MdiIcons.download), - value: useExternalDownloadManager, + 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.useExternalDownloadManager(value), + 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 _UrlCleanerSettingsTile extends StatelessWidget { + const _UrlCleanerSettingsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(MdiIcons.broom), + title: const Text('URL Cleaner'), + subtitle: const Text('Tracking removal rules and catalog updates'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await UrlCleanerSettingsRoute().push(context); + }, + ); + } +} + +class _UnshortenerSettingsTile extends StatelessWidget { + const _UnshortenerSettingsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(MdiIcons.linkVariant), + title: const Text('Unshortener'), + subtitle: const Text('Short link resolver and API token'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await UnshortenerSettingsRoute().push(context); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/experimental_settings.dart b/app/lib/features/settings/presentation/screens/experimental_settings.dart new file mode 100644 index 00000000..46b7b28b --- /dev/null +++ b/app/lib/features/settings/presentation/screens/experimental_settings.dart @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024-2026 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/engine_settings.dart'; +import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart'; +import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; +import 'package:weblibre/utils/exit_app.dart'; + +class ExperimentalSettingsScreen extends StatelessWidget { + const ExperimentalSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Experimental')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + SettingSection(name: 'Runtime & Startup'), + _IsolatedProcessEnabledTile(), + _AppZygoteProcessEnabledTile(), + ], + ); + }, + ), + ), + ); + } +} + +class _IsolatedProcessEnabledTile extends HookConsumerWidget { + const _IsolatedProcessEnabledTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isolatedProcessEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.isolatedProcessEnabled, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Isolated Content Process'), + subtitle: const Text( + 'Run web content in an isolated process. Requires app restart.', + ), + secondary: const Icon(MdiIcons.shieldCheck), + value: isolatedProcessEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.isolatedProcessEnabled(value), + ); + if (context.mounted) { + await _showRestartDialog(context); + } + }, + ); + } +} + +class _AppZygoteProcessEnabledTile extends HookConsumerWidget { + const _AppZygoteProcessEnabledTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appZygoteProcessEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.appZygoteProcessEnabled, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('App Zygote Process'), + subtitle: const Text( + 'Preload the content service for faster isolated process startup. Requires Android 10+ and app restart.', + ), + secondary: const Icon(MdiIcons.rocketLaunch), + value: appZygoteProcessEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.appZygoteProcessEnabled(value), + ); + if (context.mounted) { + await _showRestartDialog(context); + } + }, + ); + } +} + +Future _showRestartDialog(BuildContext context) async { + final result = await showQuitBrowserDialog(context); + if (result == true && context.mounted) { + await exitApp(ProviderScope.containerOf(context)); + } +} diff --git a/app/lib/features/settings/presentation/screens/extensions_settings.dart b/app/lib/features/settings/presentation/screens/extensions_settings.dart new file mode 100644 index 00000000..aac45719 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/extensions_settings.dart @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024-2026 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/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.dart'; +import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; +import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; + +class ExtensionsSettingsScreen extends StatelessWidget { + const ExtensionsSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Extensions')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [ + SettingSection(name: 'Extensions'), + _InstallLocalAddonTile(), + _AddonCollectionTile(), + ], + ); + }, + ), + ), + ); + } +} + +class _InstallLocalAddonTile extends StatelessWidget { + const _InstallLocalAddonTile(); + + @override + Widget build(BuildContext context) { + return CustomListTile( + title: 'Install from File', + subtitle: 'Install an extension from a local .xpi file', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + MdiIcons.puzzle, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + await showInstallLocalAddonDialog(context); + }, + icon: const Icon(Icons.file_open), + label: const Text('Install'), + ), + ); + } +} + +class _AddonCollectionTile extends StatelessWidget { + const _AddonCollectionTile(); + + @override + Widget build(BuildContext context) { + return CustomListTile( + title: 'Custom Collection', + subtitle: 'Use a custom Mozilla addon collection', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + MdiIcons.folderMultiple, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + await AddonCollectionRoute().push(context); + }, + icon: const Icon(Icons.settings), + label: const Text('Configure'), + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart b/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart deleted file mode 100644 index aa4ac142..00000000 --- a/app/lib/features/settings/presentation/screens/fingerprinting_settings.dart +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2024-2026 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 new file mode 100644 index 00000000..cf6c95ac --- /dev/null +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2024-2026 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_hooks/flutter_hooks.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 GeneralSettingsScreen extends StatelessWidget { + const GeneralSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('General')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [_AppearanceSection(), _DownloadsSection()], + ); + }, + ), + ), + ); + } +} + +class _AppearanceSection extends StatelessWidget { + const _AppearanceSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Appearance'), + _ThemeSection(), + _UiZoomSection(), + _DisableAnimationsTile(), + _ShowModalBarrierTile(), + ], + ); + } +} + +class _DownloadsSection extends StatelessWidget { + const _DownloadsSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Downloads'), + _ExternalDownloadManagerTile(), + ], + ); + } +} + +class _UiZoomSection extends HookConsumerWidget { + const _UiZoomSection(); + + static final _sliderDivisions = + ((maxUiScaleFactor - minUiScaleFactor) / uiScaleFactorStep).round(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uiScaleFactor = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.uiScaleFactor), + ); + final sliderValue = useState(uiScaleFactor); + + useEffect(() { + sliderValue.value = uiScaleFactor; + return null; + }, [uiScaleFactor]); + + final sliderLabel = '${(sliderValue.value * 100).round()}%'; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('User Interface Zoom'), + subtitle: Text('Make the user interface smaller or larger'), + leading: Icon(Icons.zoom_in), + contentPadding: EdgeInsets.zero, + ), + Row( + children: [ + Text(sliderLabel, style: Theme.of(context).textTheme.titleLarge), + Expanded( + child: Slider( + min: minUiScaleFactor, + max: maxUiScaleFactor, + divisions: _sliderDivisions, + label: sliderLabel, + value: sliderValue.value.clamp( + minUiScaleFactor, + maxUiScaleFactor, + ), + onChanged: (value) { + sliderValue.value = value; + }, + onChangeEnd: (value) async { + final normalized = _normalizeUiScale(value); + sliderValue.value = normalized; + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .uiScaleFactor(normalized), + ); + }, + ), + ), + ], + ), + ], + ), + ); + } +} + +double _normalizeUiScale(double value) { + final clampedValue = value.clamp(minUiScaleFactor, maxUiScaleFactor); + final stepIndex = ((clampedValue - minUiScaleFactor) / uiScaleFactorStep) + .round(); + final normalized = minUiScaleFactor + (stepIndex * uiScaleFactorStep); + return normalized.clamp(minUiScaleFactor, maxUiScaleFactor); +} + +class _DisableAnimationsTile extends HookConsumerWidget { + const _DisableAnimationsTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final disableAnimations = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.disableAnimations), + ); + + return SwitchListTile.adaptive( + title: const Text('Disable Animations'), + subtitle: const Text('Reduce motion and turn off app animations'), + secondary: const Icon(Icons.animation), + value: disableAnimations, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.disableAnimations(value), + ); + }, + ); + } +} + +class _ShowModalBarrierTile extends HookConsumerWidget { + const _ShowModalBarrierTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showModalBarrier = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.showModalBarrier), + ); + + return SwitchListTile.adaptive( + title: const Text('Show Modal Barrier'), + subtitle: const Text( + 'Dim the background behind dialogs and bottom sheets', + ), + secondary: const Icon(Icons.layers), + value: showModalBarrier, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.showModalBarrier(value), + ); + }, + ); + } +} + +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 _ExternalDownloadManagerTile extends HookConsumerWidget { + const _ExternalDownloadManagerTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final useExternalDownloadManager = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.useExternalDownloadManager, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Use external download manager'), + subtitle: const Text('Manage downloads with another app'), + secondary: const Icon(Icons.download), + value: useExternalDownloadManager, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.useExternalDownloadManager(value), + ); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart index 26224bc6..87d40c80 100644 --- a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart +++ b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart @@ -49,13 +49,13 @@ class PrivacySecuritySettingsScreen extends StatelessWidget { controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _PrivacyModesSection(), _TrackingProtectionSection(), - _OpenLinkModulesSection(), + _FingerprintingSection(), _ConnectionSecuritySection(), - _LocalNetworkAccessSection(), + _NetworkProtectionSection(), + _PrivacySignalsSection(), _DataManagementSection(), - _AdvancedSection(), + _AdvancedSecuritySection(), ], ); }, @@ -65,16 +65,17 @@ class PrivacySecuritySettingsScreen extends StatelessWidget { } } -class _PrivacyModesSection extends StatelessWidget { - const _PrivacyModesSection(); +class _FingerprintingSection extends StatelessWidget { + const _FingerprintingSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Privacy Modes'), - _IncognitoModeSection(), - _GlobalPrivacyControlTile(), + SettingSection(name: 'Fingerprinting'), + _BrowserLanguagesTile(), + _FingerprintProtectionTile(), + _ResistFingerprintingTile(), ], ); } @@ -129,55 +130,21 @@ class _ConnectionSecuritySection extends StatelessWidget { } } -class _OpenLinkModulesSection extends StatelessWidget { - const _OpenLinkModulesSection(); +class _PrivacySignalsSection extends StatelessWidget { + const _PrivacySignalsSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Open Link Modules'), - _UrlCleanerSettingsTile(), - _UnshortenerSettingsTile(), + SettingSection(name: 'Privacy Signals & Modes'), + _IncognitoModeSection(), + _GlobalPrivacyControlTile(), ], ); } } -class _UrlCleanerSettingsTile extends StatelessWidget { - const _UrlCleanerSettingsTile(); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: const Icon(MdiIcons.broom), - title: const Text('URL Cleaner'), - subtitle: const Text('Tracking removal rules and catalog updates'), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await UrlCleanerSettingsRoute().push(context); - }, - ); - } -} - -class _UnshortenerSettingsTile extends StatelessWidget { - const _UnshortenerSettingsTile(); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: const Icon(MdiIcons.linkVariant), - title: const Text('Unshortener'), - subtitle: const Text('Short link resolver and API token'), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - await UnshortenerSettingsRoute().push(context); - }, - ); - } -} - class _DataManagementSection extends StatelessWidget { const _DataManagementSection(); @@ -194,14 +161,14 @@ class _DataManagementSection extends StatelessWidget { } } -class _AdvancedSection extends StatelessWidget { - const _AdvancedSection(); +class _AdvancedSecuritySection extends StatelessWidget { + const _AdvancedSecuritySection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Advanced'), + SettingSection(name: 'Advanced Security'), _WebEngineHardeningTile(), _FissionEnabledTile(), _ExtensionsWebAPIEnabledTile(), @@ -812,14 +779,14 @@ Future _showRestartDialog(BuildContext context, WidgetRef ref) async { } } -class _LocalNetworkAccessSection extends StatelessWidget { - const _LocalNetworkAccessSection(); +class _NetworkProtectionSection extends StatelessWidget { + const _NetworkProtectionSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Local Network / Device Access Blocking'), + SettingSection(name: 'Network Protection'), _LnaEnabledTile(), _LnaBlockingTile(), _LnaBlockTrackersTile(), @@ -828,6 +795,73 @@ class _LocalNetworkAccessSection extends StatelessWidget { } } +class _BrowserLanguagesTile extends StatelessWidget { + const _BrowserLanguagesTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text('Browser Languages'), + subtitle: const Text( + 'Configure language preferences exposed to websites', + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(Icons.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); + }, + ); + } +} + class _LnaEnabledTile extends HookConsumerWidget { const _LnaEnabledTile(); diff --git a/app/lib/features/settings/presentation/screens/search_content_settings.dart b/app/lib/features/settings/presentation/screens/search_settings.dart similarity index 63% rename from app/lib/features/settings/presentation/screens/search_content_settings.dart rename to app/lib/features/settings/presentation/screens/search_settings.dart index 5ed5dd50..21e82dce 100644 --- a/app/lib/features/settings/presentation/screens/search_content_settings.dart +++ b/app/lib/features/settings/presentation/screens/search_settings.dart @@ -29,18 +29,16 @@ import 'package:weblibre/features/settings/presentation/controllers/save_setting 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}); +class SearchSettingsScreen extends StatelessWidget { + const SearchSettingsScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Search & Content')), + appBar: AppBar(title: const Text('Search')), body: SafeArea( child: FadingScroll( fadingSize: 25, @@ -49,10 +47,9 @@ class SearchContentSettingsScreen extends StatelessWidget { controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _SearchSection(), - _ContentViewingSection(), - _ContentEnhancementSection(), - _ExtensionsSection(), + _ProvidersSection(), + _BangShortcutsSection(), + _HistorySuggestionsSection(), ], ); }, @@ -62,18 +59,44 @@ class SearchContentSettingsScreen extends StatelessWidget { } } -class _SearchSection extends StatelessWidget { - const _SearchSection(); +class _ProvidersSection extends StatelessWidget { + const _ProvidersSection(); @override Widget build(BuildContext context) { return const Column( children: [ - SettingSection(name: 'Search'), + SettingSection(name: 'Providers'), _DefaultSearchProviderSection(), - _CustomSearchEnginesTile(), - _BangsTile(), _AutocompleteProviderSection(), + _CustomSearchEnginesTile(), + ], + ); + } +} + +class _BangShortcutsSection extends StatelessWidget { + const _BangShortcutsSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Bang Shortcuts'), + _BangsTile(), + ], + ); + } +} + +class _HistorySuggestionsSection extends StatelessWidget { + const _HistorySuggestionsSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'History & Suggestions'), _MaxSearchHistoryEntriesSection(), _AllowClipboardAccessTile(), ], @@ -81,50 +104,6 @@ class _SearchSection extends StatelessWidget { } } -class _ContentViewingSection extends StatelessWidget { - const _ContentViewingSection(); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SettingSection(name: 'Content Viewing'), - _PdfViewerTile(), - _EnableReaderModeTile(), - _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(); @@ -243,8 +222,8 @@ class _BangsTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - title: const Text('Bangs'), - subtitle: const Text('Search shortcuts management'), + title: const Text('Bang Settings'), + subtitle: const Text('Manage bang repositories and usage data'), contentPadding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, @@ -324,147 +303,6 @@ class _MaxSearchHistoryEntriesSection extends HookConsumerWidget { } } -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 _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); - }, - ); - } -} - class _AllowClipboardAccessTile extends HookConsumerWidget { const _AllowClipboardAccessTile(); diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index 4eeae3a5..50aec927 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -37,12 +37,14 @@ class SettingsScreen extends HookConsumerWidget { controller: controller, padding: const EdgeInsets.symmetric(horizontal: 12.0), children: const [ - _AppearanceDisplayTile(), + _GeneralTile(), + _BrowsingTile(), + _ToolbarLayoutTile(), + _WebContentTile(), + _SearchTile(), _PrivacySecurityTile(), - _SearchContentTile(), - _TabsBehaviorTile(), + _ExtensionsTile(), _SyncTile(), - _FingerprintingTile(), _AdvancedTile(), ], ); @@ -52,8 +54,8 @@ class SettingsScreen extends HookConsumerWidget { } } -class _AppearanceDisplayTile extends StatelessWidget { - const _AppearanceDisplayTile(); +class _GeneralTile extends StatelessWidget { + const _GeneralTile(); @override Widget build(BuildContext context) { @@ -61,16 +63,66 @@ class _AppearanceDisplayTile extends StatelessWidget { color: Theme.of(context).colorScheme.surfaceContainerHigh, clipBehavior: Clip.antiAlias, child: ListTile( - title: const Text('Appearance & Display'), - subtitle: const Text('Theme, tabs, toolbars, gestures'), + title: const Text('General'), + subtitle: const Text('Appearance, language, downloads'), contentPadding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, ), - leading: const Icon(Icons.palette), + leading: const Icon(Icons.tune), trailing: const Icon(Icons.chevron_right), onTap: () async { - await AppearanceDisplaySettingsRoute().push(context); + await GeneralSettingsRoute().push(context); + }, + ), + ); + } +} + +class _BrowsingTile extends StatelessWidget { + const _BrowsingTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Browsing'), + subtitle: const Text('Tabs, navigation, external links'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.compassOutline), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await BrowsingSettingsRoute().push(context); + }, + ), + ); + } +} + +class _ToolbarLayoutTile extends StatelessWidget { + const _ToolbarLayoutTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Toolbar & Layout'), + subtitle: const Text('Tab bar, toolbar, quick switcher, tab view'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.viewDashboardOutline), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await ToolbarLayoutSettingsRoute().push(context); }, ), ); @@ -102,8 +154,8 @@ class _PrivacySecurityTile extends StatelessWidget { } } -class _SearchContentTile extends StatelessWidget { - const _SearchContentTile(); +class _WebContentTile extends StatelessWidget { + const _WebContentTile(); @override Widget build(BuildContext context) { @@ -111,8 +163,33 @@ class _SearchContentTile extends StatelessWidget { color: Theme.of(context).colorScheme.surfaceContainerHigh, clipBehavior: Clip.antiAlias, child: ListTile( - title: const Text('Search & Content'), - subtitle: const Text('Search providers, reader mode, AI'), + title: const Text('Web Content'), + subtitle: const Text('Page display, PDF, reader mode, AI'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.fileDocumentOutline), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await WebContentSettingsRoute().push(context); + }, + ), + ); + } +} + +class _SearchTile extends StatelessWidget { + const _SearchTile(); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Search'), + subtitle: const Text('Providers, bangs, search history'), contentPadding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, @@ -120,15 +197,15 @@ class _SearchContentTile extends StatelessWidget { leading: const Icon(MdiIcons.magnify), trailing: const Icon(Icons.chevron_right), onTap: () async { - await SearchContentSettingsRoute().push(context); + await SearchSettingsRoute().push(context); }, ), ); } } -class _TabsBehaviorTile extends StatelessWidget { - const _TabsBehaviorTile(); +class _ExtensionsTile extends StatelessWidget { + const _ExtensionsTile(); @override Widget build(BuildContext context) { @@ -136,41 +213,16 @@ class _TabsBehaviorTile extends StatelessWidget { color: Theme.of(context).colorScheme.surfaceContainerHigh, clipBehavior: Clip.antiAlias, child: ListTile( - title: const Text('Tabs & Behavior'), - subtitle: const Text('New tab defaults, link handling'), + title: const Text('Extensions'), + subtitle: const Text('Install and manage extension sources'), contentPadding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, ), - leading: const Icon(MdiIcons.tab), + leading: const Icon(MdiIcons.puzzleOutline), 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).colorScheme.surfaceContainerHigh, - 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); + await ExtensionsSettingsRoute().push(context); }, ), ); diff --git a/app/lib/features/settings/presentation/screens/tab_bar_settings.dart b/app/lib/features/settings/presentation/screens/toolbar_layout_settings.dart similarity index 90% rename from app/lib/features/settings/presentation/screens/tab_bar_settings.dart rename to app/lib/features/settings/presentation/screens/toolbar_layout_settings.dart index 3f799fb9..d3e0fd69 100644 --- a/app/lib/features/settings/presentation/screens/tab_bar_settings.dart +++ b/app/lib/features/settings/presentation/screens/toolbar_layout_settings.dart @@ -33,18 +33,19 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.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 TabBarSettingsScreen extends HookConsumerWidget { - const TabBarSettingsScreen({super.key}); +class ToolbarLayoutSettingsScreen extends HookConsumerWidget { + const ToolbarLayoutSettingsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(generalSettingsWithDefaultsProvider); return Scaffold( - appBar: AppBar(title: const Text('Tab Bar')), + appBar: AppBar(title: const Text('Toolbar & Layout')), body: SafeArea( child: FadingScroll( fadingSize: 25, @@ -58,7 +59,7 @@ class TabBarSettingsScreen extends HookConsumerWidget { ), const SliverPadding( padding: EdgeInsets.symmetric(horizontal: 12.0), - sliver: SliverToBoxAdapter(child: _TabBarLayoutSection()), + sliver: SliverToBoxAdapter(child: _ToolbarLayoutContent()), ), ], ); @@ -69,22 +70,28 @@ class TabBarSettingsScreen extends HookConsumerWidget { } } -class _TabBarLayoutSection extends StatelessWidget { - const _TabBarLayoutSection(); +class _ToolbarLayoutContent extends StatelessWidget { + const _ToolbarLayoutContent(); @override Widget build(BuildContext context) { return const Column( children: [ + SettingSection(name: 'Tab Bar'), _TabBarPositionSection(), _TabBarLayoutModeSection(), + _AutoHideTabBarTile(), + SettingSection(name: 'Contextual Toolbar'), _ShowContextualTabBarTile(), _CustomizeToolbarButtonsTile(), - _AutoHideTabBarTile(), + SettingSection(name: 'Quick Tab Switcher'), _ShowQuickTabSwitcherBarTile(), _QuickTabSwitcherModeSection(), _QuickTabSwitcherHistorySuggestionsTile(), _QuickTabSwitcherShowTitlesTile(), + SettingSection(name: 'Tab View'), + _BottomSheetTabViewTile(), + _TabListShowFaviconsTile(), ], ); } @@ -93,7 +100,7 @@ class _TabBarLayoutSection extends StatelessWidget { class _TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate { const _TabBarPreviewHeaderDelegate({required this.settings}); - static const _kPreviewBaseHeight = 160.0; + static const _kPreviewBaseHeight = 164.0; final GeneralSettings settings; @@ -321,7 +328,9 @@ class _TabBarPreviewCard extends HookWidget { children: [ const ListTile( title: Text('Live Preview'), - subtitle: Text('Reflects your current tab bar settings'), + subtitle: Text( + 'Reflects your current toolbar and layout settings', + ), leading: Icon(MdiIcons.televisionGuide), contentPadding: EdgeInsets.symmetric(horizontal: 8.0), ), @@ -596,7 +605,7 @@ class _ShowContextualTabBarTile extends HookConsumerWidget { ); return SwitchListTile.adaptive( - title: const Text('Show Contextual Tab Bar'), + title: const Text('Show Contextual Toolbar'), subtitle: const Text( 'Show additional bottom toolbar for navigation and actions', ), @@ -745,3 +754,59 @@ class _AutoHideTabBarTile extends HookConsumerWidget { ); } } + +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 _TabListShowFaviconsTile extends HookConsumerWidget { + const _TabListShowFaviconsTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabListShowFavicons = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabListShowFavicons), + ); + + return SwitchListTile.adaptive( + title: const Text('Show Favicons in List View'), + subtitle: const Text( + 'Display website icons instead of page thumbnails in tab list view', + ), + secondary: const Icon(MdiIcons.web), + value: tabListShowFavicons, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabListShowFavicons(value), + ); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/web_content_settings.dart b/app/lib/features/settings/presentation/screens/web_content_settings.dart new file mode 100644 index 00000000..0de57a9f --- /dev/null +++ b/app/lib/features/settings/presentation/screens/web_content_settings.dart @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2024-2026 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_hooks/flutter_hooks.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/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 WebContentSettingsScreen extends StatelessWidget { + const WebContentSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Web Content')), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), + children: const [_DisplaySection(), _ContentFeaturesSection()], + ); + }, + ), + ), + ); + } +} + +class _DisplaySection extends StatelessWidget { + const _DisplaySection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Display'), + _WebFontsEnabledTile(), + _AutomaticFontSizeAdjustmentTile(), + _FontSizeFactorSlider(), + _FontInflationTile(), + _InputAutoZoomEnabledTile(), + ], + ); + } +} + +class _ContentFeaturesSection extends StatelessWidget { + const _ContentFeaturesSection(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + SettingSection(name: 'Content Features'), + _PdfViewerTile(), + _EnableReaderModeTile(), + _EnforceReaderModeTile(), + _OnDeviceAiTile(), + ], + ); + } +} + +class _WebFontsEnabledTile extends HookConsumerWidget { + const _WebFontsEnabledTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final webFontsEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.webFontsEnabled), + ); + + return SwitchListTile.adaptive( + title: const Text('Web Fonts'), + subtitle: const Text('Allow websites to use custom fonts'), + secondary: const Icon(MdiIcons.formatFont), + value: webFontsEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.webFontsEnabled(value), + ); + }, + ); + } +} + +class _AutomaticFontSizeAdjustmentTile extends HookConsumerWidget { + const _AutomaticFontSizeAdjustmentTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final automaticFontSizeAdjustment = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.automaticFontSizeAdjustment, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Automatic Font Size'), + subtitle: const Text( + 'Automatically adjust font size based on system settings. Disable to manually control font size factor and inflation.', + ), + secondary: const Icon(MdiIcons.formatFontSizeIncrease), + value: automaticFontSizeAdjustment, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.automaticFontSizeAdjustment(value), + ); + }, + ); + } +} + +class _FontSizeFactorSlider extends HookConsumerWidget { + const _FontSizeFactorSlider(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final automaticFontSizeAdjustment = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.automaticFontSizeAdjustment, + ), + ); + final fontSizeFactor = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.fontSizeFactor), + ); + final sliderValue = useState(fontSizeFactor); + + useEffect(() { + sliderValue.value = fontSizeFactor; + return null; + }, [fontSizeFactor]); + + final sliderLabel = '${(sliderValue.value * 100).round()}%'; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: const Text('Font Size Factor'), + subtitle: Text( + automaticFontSizeAdjustment + ? 'Disabled while automatic font size is enabled' + : 'Scale web page text size', + ), + leading: const Icon(MdiIcons.formatSize), + contentPadding: EdgeInsets.zero, + enabled: !automaticFontSizeAdjustment, + ), + Row( + children: [ + Text( + sliderLabel, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: automaticFontSizeAdjustment + ? Theme.of(context).disabledColor + : null, + ), + ), + Expanded( + child: Slider( + min: 0.5, + max: 3.0, + divisions: 25, + label: sliderLabel, + value: sliderValue.value.clamp(0.5, 3.0), + onChanged: automaticFontSizeAdjustment + ? null + : (value) { + sliderValue.value = value; + }, + onChangeEnd: automaticFontSizeAdjustment + ? null + : (value) async { + final rounded = (value * 10).round() / 10; + sliderValue.value = rounded; + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .fontSizeFactor(rounded), + ); + }, + ), + ), + ], + ), + ], + ), + ); + } +} + +class _FontInflationTile extends HookConsumerWidget { + const _FontInflationTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final automaticFontSizeAdjustment = ref.watch( + engineSettingsWithDefaultsProvider.select( + (s) => s.automaticFontSizeAdjustment, + ), + ); + final fontInflationEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.fontInflationEnabled), + ); + + return SwitchListTile.adaptive( + title: const Text('Font Inflation'), + subtitle: Text( + automaticFontSizeAdjustment + ? 'Disabled while automatic font size is enabled' + : 'Enlarge text on pages that lack a mobile viewport meta tag', + ), + secondary: const Icon(MdiIcons.formatTextVariantOutline), + value: fontInflationEnabled, + onChanged: automaticFontSizeAdjustment + ? null + : (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.fontInflationEnabled(value), + ); + }, + ); + } +} + +class _InputAutoZoomEnabledTile extends HookConsumerWidget { + const _InputAutoZoomEnabledTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final inputAutoZoomEnabled = ref.watch( + engineSettingsWithDefaultsProvider.select((s) => s.inputAutoZoomEnabled), + ); + + return SwitchListTile.adaptive( + title: const Text('Input Auto Zoom'), + subtitle: const Text('Automatically zoom in when focusing text inputs'), + secondary: const Icon(MdiIcons.formTextbox), + value: inputAutoZoomEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.inputAutoZoomEnabled(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 _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 _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), + ); + }, + ); + } +}