reorganize settings

This commit is contained in:
Fabian Freund
2026-03-13 06:21:15 +01:00
parent 8a07d7df1a
commit 3664dca11f
15 changed files with 1510 additions and 1410 deletions
+7 -5
View File
@@ -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/onboarding/presentation/onboarding.dart';
import 'package:weblibre/features/settings/presentation/screens/addon_collection.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/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/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/contextual_toolbar_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.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/doh_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.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/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/locale_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/privacy_security_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/settings.dart';
import 'package:weblibre/features/settings/presentation/screens/tab_bar_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.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.dart';
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
import 'package:weblibre/features/sync/presentation/screens/sync_settings.dart'; import 'package:weblibre/features/sync/presentation/screens/sync_settings.dart';
+87 -35
View File
@@ -1375,14 +1375,14 @@ RouteBase get $settingsRoute => GoRouteData.$route(
factory: $SettingsRoute._fromState, factory: $SettingsRoute._fromState,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: 'appearance_display', path: 'general',
name: 'AppearanceDisplaySettingsRoute', name: 'GeneralSettingsRoute',
factory: $AppearanceDisplaySettingsRoute._fromState, factory: $GeneralSettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'tab_bar', path: 'browsing',
name: 'TabBarSettingsRoute', name: 'BrowsingSettingsRoute',
factory: $TabBarSettingsRoute._fromState, factory: $BrowsingSettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'privacy_security', path: 'privacy_security',
@@ -1390,25 +1390,35 @@ RouteBase get $settingsRoute => GoRouteData.$route(
factory: $PrivacySecuritySettingsRoute._fromState, factory: $PrivacySecuritySettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'search_content', path: 'toolbar_layout',
name: 'SearchContentSettingsRoute', name: 'ToolbarLayoutSettingsRoute',
factory: $SearchContentSettingsRoute._fromState, factory: $ToolbarLayoutSettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'tabs_behavior', path: 'web_content',
name: 'TabsBehaviorSettingsRoute', name: 'WebContentSettingsRoute',
factory: $TabsBehaviorSettingsRoute._fromState, factory: $WebContentSettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'fingerprinting', path: 'search',
name: 'FingerprintingSettingsRoute', name: 'SearchSettingsRoute',
factory: $FingerprintingSettingsRoute._fromState, factory: $SearchSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'extensions',
name: 'ExtensionsSettingsRoute',
factory: $ExtensionsSettingsRoute._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'advanced', path: 'advanced',
name: 'AdvancedSettingsRoute', name: 'AdvancedSettingsRoute',
factory: $AdvancedSettingsRoute._fromState, factory: $AdvancedSettingsRoute._fromState,
), ),
GoRouteData.$route(
path: 'experimental',
name: 'ExperimentalSettingsRoute',
factory: $ExperimentalSettingsRoute._fromState,
),
GoRouteData.$route( GoRouteData.$route(
path: 'bang', path: 'bang',
name: 'BangSettingsRoute', name: 'BangSettingsRoute',
@@ -1504,12 +1514,12 @@ mixin $SettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
mixin $AppearanceDisplaySettingsRoute on GoRouteData { mixin $GeneralSettingsRoute on GoRouteData {
static AppearanceDisplaySettingsRoute _fromState(GoRouterState state) => static GeneralSettingsRoute _fromState(GoRouterState state) =>
AppearanceDisplaySettingsRoute(); GeneralSettingsRoute();
@override @override
String get location => GoRouteData.$location('/settings/appearance_display'); String get location => GoRouteData.$location('/settings/general');
@override @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -1525,12 +1535,12 @@ mixin $AppearanceDisplaySettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
mixin $TabBarSettingsRoute on GoRouteData { mixin $BrowsingSettingsRoute on GoRouteData {
static TabBarSettingsRoute _fromState(GoRouterState state) => static BrowsingSettingsRoute _fromState(GoRouterState state) =>
TabBarSettingsRoute(); BrowsingSettingsRoute();
@override @override
String get location => GoRouteData.$location('/settings/tab_bar'); String get location => GoRouteData.$location('/settings/browsing');
@override @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -1567,12 +1577,12 @@ mixin $PrivacySecuritySettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
mixin $SearchContentSettingsRoute on GoRouteData { mixin $ToolbarLayoutSettingsRoute on GoRouteData {
static SearchContentSettingsRoute _fromState(GoRouterState state) => static ToolbarLayoutSettingsRoute _fromState(GoRouterState state) =>
SearchContentSettingsRoute(); ToolbarLayoutSettingsRoute();
@override @override
String get location => GoRouteData.$location('/settings/search_content'); String get location => GoRouteData.$location('/settings/toolbar_layout');
@override @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -1588,12 +1598,12 @@ mixin $SearchContentSettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
mixin $TabsBehaviorSettingsRoute on GoRouteData { mixin $WebContentSettingsRoute on GoRouteData {
static TabsBehaviorSettingsRoute _fromState(GoRouterState state) => static WebContentSettingsRoute _fromState(GoRouterState state) =>
TabsBehaviorSettingsRoute(); WebContentSettingsRoute();
@override @override
String get location => GoRouteData.$location('/settings/tabs_behavior'); String get location => GoRouteData.$location('/settings/web_content');
@override @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -1609,12 +1619,33 @@ mixin $TabsBehaviorSettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
mixin $FingerprintingSettingsRoute on GoRouteData { mixin $SearchSettingsRoute on GoRouteData {
static FingerprintingSettingsRoute _fromState(GoRouterState state) => static SearchSettingsRoute _fromState(GoRouterState state) =>
FingerprintingSettingsRoute(); SearchSettingsRoute();
@override @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<T?> push<T>(BuildContext context) => context.push<T>(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 @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -1651,6 +1682,27 @@ mixin $AdvancedSettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location); 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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin $BangSettingsRoute on GoRouteData { mixin $BangSettingsRoute on GoRouteData {
static BangSettingsRoute _fromState(GoRouterState state) => static BangSettingsRoute _fromState(GoRouterState state) =>
BangSettingsRoute(); BangSettingsRoute();
+51 -29
View File
@@ -23,34 +23,42 @@ part of 'routes.dart';
name: 'SettingsRoute', name: 'SettingsRoute',
path: '/settings', path: '/settings',
routes: [ routes: [
TypedGoRoute<AppearanceDisplaySettingsRoute>( TypedGoRoute<GeneralSettingsRoute>(
name: 'AppearanceDisplaySettingsRoute', name: 'GeneralSettingsRoute',
path: 'appearance_display', path: 'general',
), ),
TypedGoRoute<TabBarSettingsRoute>( TypedGoRoute<BrowsingSettingsRoute>(
name: 'TabBarSettingsRoute', name: 'BrowsingSettingsRoute',
path: 'tab_bar', path: 'browsing',
), ),
TypedGoRoute<PrivacySecuritySettingsRoute>( TypedGoRoute<PrivacySecuritySettingsRoute>(
name: 'PrivacySecuritySettingsRoute', name: 'PrivacySecuritySettingsRoute',
path: 'privacy_security', path: 'privacy_security',
), ),
TypedGoRoute<SearchContentSettingsRoute>( TypedGoRoute<ToolbarLayoutSettingsRoute>(
name: 'SearchContentSettingsRoute', name: 'ToolbarLayoutSettingsRoute',
path: 'search_content', path: 'toolbar_layout',
), ),
TypedGoRoute<TabsBehaviorSettingsRoute>( TypedGoRoute<WebContentSettingsRoute>(
name: 'TabsBehaviorSettingsRoute', name: 'WebContentSettingsRoute',
path: 'tabs_behavior', path: 'web_content',
), ),
TypedGoRoute<FingerprintingSettingsRoute>( TypedGoRoute<SearchSettingsRoute>(
name: 'FingerprintingSettingsRoute', name: 'SearchSettingsRoute',
path: 'fingerprinting', path: 'search',
),
TypedGoRoute<ExtensionsSettingsRoute>(
name: 'ExtensionsSettingsRoute',
path: 'extensions',
), ),
TypedGoRoute<AdvancedSettingsRoute>( TypedGoRoute<AdvancedSettingsRoute>(
name: 'AdvancedSettingsRoute', name: 'AdvancedSettingsRoute',
path: 'advanced', path: 'advanced',
), ),
TypedGoRoute<ExperimentalSettingsRoute>(
name: 'ExperimentalSettingsRoute',
path: 'experimental',
),
TypedGoRoute<BangSettingsRoute>(name: 'BangSettingsRoute', path: 'bang'), TypedGoRoute<BangSettingsRoute>(name: 'BangSettingsRoute', path: 'bang'),
TypedGoRoute<WebEngineHardeningRoute>( TypedGoRoute<WebEngineHardeningRoute>(
name: 'WebEngineHardeningRoute', name: 'WebEngineHardeningRoute',
@@ -106,18 +114,17 @@ class SettingsRoute extends GoRouteData with $SettingsRoute {
} }
} }
class AppearanceDisplaySettingsRoute extends GoRouteData class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute {
with $AppearanceDisplaySettingsRoute {
@override @override
Widget build(BuildContext context, GoRouterState state) { 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 @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const TabBarSettingsScreen(); return const BrowsingSettingsScreen();
} }
} }
@@ -129,27 +136,34 @@ class PrivacySecuritySettingsRoute extends GoRouteData
} }
} }
class SearchContentSettingsRoute extends GoRouteData class ToolbarLayoutSettingsRoute extends GoRouteData
with $SearchContentSettingsRoute { with $ToolbarLayoutSettingsRoute {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const SearchContentSettingsScreen(); return const ToolbarLayoutSettingsScreen();
} }
} }
class TabsBehaviorSettingsRoute extends GoRouteData class WebContentSettingsRoute extends GoRouteData
with $TabsBehaviorSettingsRoute { with $WebContentSettingsRoute {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const TabsBehaviorSettingsScreen(); return const WebContentSettingsScreen();
} }
} }
class FingerprintingSettingsRoute extends GoRouteData class SearchSettingsRoute extends GoRouteData with $SearchSettingsRoute {
with $FingerprintingSettingsRoute {
@override @override
Widget build(BuildContext context, GoRouterState state) { 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 { class BangSettingsRoute extends GoRouteData with $BangSettingsRoute {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
@@ -29,13 +29,11 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/providers/app_state.dart'; import 'package:weblibre/core/providers/app_state.dart';
import 'package:weblibre/core/routing/routes.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/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/dialogs/user_agent_restart_dialog.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/custom_list_tile.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.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/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/providers.dart';
import 'package:weblibre/features/user/domain/repositories/cache.dart'; import 'package:weblibre/features/user/domain/repositories/cache.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
@@ -57,11 +55,9 @@ class AdvancedSettingsScreen extends StatelessWidget {
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [ children: const [
_ContentBehaviorSection(), _ContentIdentitySection(),
_ExperimentalSection(), _ExperimentalSection(),
_ExtensionsSection(), _DeveloperToolsSection(),
_StorageDebuggingSection(),
_ResetSection(),
], ],
); );
}, },
@@ -71,14 +67,14 @@ class AdvancedSettingsScreen extends StatelessWidget {
} }
} }
class _ContentBehaviorSection extends StatelessWidget { class _ContentIdentitySection extends StatelessWidget {
const _ContentBehaviorSection(); const _ContentIdentitySection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Content Behavior'), SettingSection(name: 'Content & Identity'),
_JavaScriptTile(), _JavaScriptTile(),
_UserAgentTile(), _UserAgentTile(),
_EnterpriseRootsTile(), _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 { class _ExperimentalSection extends StatelessWidget {
const _ExperimentalSection(); const _ExperimentalSection();
@@ -110,91 +91,23 @@ class _ExperimentalSection extends StatelessWidget {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Experimental'), SettingSection(name: 'Experimental'),
_IsolatedProcessEnabledTile(), _ExperimentalSettingsTile(),
_AppZygoteProcessEnabledTile(),
], ],
); );
} }
} }
class _InstallLocalAddonTile extends StatelessWidget { class _DeveloperToolsSection extends StatelessWidget {
const _InstallLocalAddonTile(); const _DeveloperToolsSection();
@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();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Storage & Debugging'), SettingSection(name: 'Developer Tools'),
_IconCacheTile(), _IconCacheTile(),
_ErrorLogsTile(), _ErrorLogsTile(),
_DartVmTile(), _DartVmTile(),
],
);
}
}
class _ResetSection extends StatelessWidget {
const _ResetSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Reset'),
_ResetUITile(), _ResetUITile(),
], ],
); );
@@ -303,67 +216,22 @@ class _EnterpriseRootsTile extends HookConsumerWidget {
} }
} }
class _IsolatedProcessEnabledTile extends HookConsumerWidget { class _ExperimentalSettingsTile extends StatelessWidget {
const _IsolatedProcessEnabledTile(); const _ExperimentalSettingsTile();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final isolatedProcessEnabled = ref.watch( return ListTile(
engineSettingsWithDefaultsProvider.select( title: const Text('Experimental Features'),
(s) => s.isolatedProcessEnabled, subtitle: const Text('Low-level runtime features and startup behavior'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
), ),
); leading: const Icon(MdiIcons.flaskOutline),
trailing: const Icon(Icons.chevron_right),
return SwitchListTile.adaptive( onTap: () async {
title: const Text('Isolated Content Process'), await ExperimentalSettingsRoute().push(context);
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);
}
}, },
); );
} }
@@ -511,10 +379,3 @@ class _ResetUITile extends ConsumerWidget {
); );
} }
} }
Future<void> _showRestartDialog(BuildContext context, WidgetRef ref) async {
final result = await showQuitBrowserDialog(context);
if (result == true && context.mounted) {
await exitApp(ProviderScope.containerOf(context));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThemeMode>(
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),
);
},
);
}
}
@@ -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/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class TabsBehaviorSettingsScreen extends StatelessWidget { class BrowsingSettingsScreen extends StatelessWidget {
const TabsBehaviorSettingsScreen({super.key}); const BrowsingSettingsScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Tabs & Behavior')), appBar: AppBar(title: const Text('Browsing')),
body: SafeArea( body: SafeArea(
child: FadingScroll( child: FadingScroll(
fadingSize: 25, fadingSize: 25,
@@ -46,10 +46,9 @@ class TabsBehaviorSettingsScreen extends StatelessWidget {
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [ children: const [
_TabCreationSection(), _TabsSection(),
_TabOrganizationSection(), _NavigationSection(),
_TabInteractionSection(), _ExternalLinksSection(),
_DownloadsSection(),
], ],
); );
}, },
@@ -59,31 +58,16 @@ class TabsBehaviorSettingsScreen extends StatelessWidget {
} }
} }
class _TabCreationSection extends StatelessWidget { class _TabsSection extends StatelessWidget {
const _TabCreationSection(); const _TabsSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Tab Creation'), SettingSection(name: 'Tabs'),
_NewTabDefaultSection(), _NewTabDefaultSection(),
_NewTabPositionSection(), _NewTabPositionSection(),
_ExternalLinkHandlingSection(),
_AppLinksModeSection(),
],
);
}
}
class _TabOrganizationSection extends StatelessWidget {
const _TabOrganizationSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tab Organization'),
_ShowContainerUiTile(), _ShowContainerUiTile(),
_ShowIsolatedTabUiTile(), _ShowIsolatedTabUiTile(),
_CreateChildTabsTile(), _CreateChildTabsTile(),
@@ -92,15 +76,34 @@ class _TabOrganizationSection extends StatelessWidget {
} }
} }
class _TabInteractionSection extends StatelessWidget { class _NavigationSection extends StatelessWidget {
const _TabInteractionSection(); const _NavigationSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Tab Interaction'), SettingSection(name: 'Navigation'),
_PullToRefreshTile(),
_DoubleBackCloseTabTile(),
_TabBarSwipeBehaviorSection(), _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 { class _PullToRefreshTile extends HookConsumerWidget {
const _DownloadsSection(); const _PullToRefreshTile();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Downloads'),
_ExternalDownloadManagerTile(),
],
);
}
}
class _ExternalDownloadManagerTile extends HookConsumerWidget {
const _ExternalDownloadManagerTile();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final useExternalDownloadManager = ref.watch( final pullToRefreshEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select( generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled),
(s) => s.useExternalDownloadManager,
),
); );
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
title: const Text('Use external download manager'), title: const Text('Pull to Refresh'),
subtitle: const Text('Manage downloads with another app'), subtitle: const Text('Swipe down on pages to reload them'),
secondary: const Icon(MdiIcons.download), secondary: const Icon(MdiIcons.gestureSwipeDown),
value: useExternalDownloadManager, value: pullToRefreshEnabled,
onChanged: (value) async { onChanged: (value) async {
await ref await ref
.read(saveGeneralSettingsControllerProvider.notifier) .read(saveGeneralSettingsControllerProvider.notifier)
.save( .save(
(currentSettings) => (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);
},
);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<void> _showRestartDialog(BuildContext context) async {
final result = await showQuitBrowserDialog(context);
if (result == true && context.mounted) {
await exitApp(ProviderScope.containerOf(context));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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'),
),
);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
},
);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThemeMode>(
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),
);
},
);
}
}
@@ -49,13 +49,13 @@ class PrivacySecuritySettingsScreen extends StatelessWidget {
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [ children: const [
_PrivacyModesSection(),
_TrackingProtectionSection(), _TrackingProtectionSection(),
_OpenLinkModulesSection(), _FingerprintingSection(),
_ConnectionSecuritySection(), _ConnectionSecuritySection(),
_LocalNetworkAccessSection(), _NetworkProtectionSection(),
_PrivacySignalsSection(),
_DataManagementSection(), _DataManagementSection(),
_AdvancedSection(), _AdvancedSecuritySection(),
], ],
); );
}, },
@@ -65,16 +65,17 @@ class PrivacySecuritySettingsScreen extends StatelessWidget {
} }
} }
class _PrivacyModesSection extends StatelessWidget { class _FingerprintingSection extends StatelessWidget {
const _PrivacyModesSection(); const _FingerprintingSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Privacy Modes'), SettingSection(name: 'Fingerprinting'),
_IncognitoModeSection(), _BrowserLanguagesTile(),
_GlobalPrivacyControlTile(), _FingerprintProtectionTile(),
_ResistFingerprintingTile(),
], ],
); );
} }
@@ -129,55 +130,21 @@ class _ConnectionSecuritySection extends StatelessWidget {
} }
} }
class _OpenLinkModulesSection extends StatelessWidget { class _PrivacySignalsSection extends StatelessWidget {
const _OpenLinkModulesSection(); const _PrivacySignalsSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Open Link Modules'), SettingSection(name: 'Privacy Signals & Modes'),
_UrlCleanerSettingsTile(), _IncognitoModeSection(),
_UnshortenerSettingsTile(), _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 { class _DataManagementSection extends StatelessWidget {
const _DataManagementSection(); const _DataManagementSection();
@@ -194,14 +161,14 @@ class _DataManagementSection extends StatelessWidget {
} }
} }
class _AdvancedSection extends StatelessWidget { class _AdvancedSecuritySection extends StatelessWidget {
const _AdvancedSection(); const _AdvancedSecuritySection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Advanced'), SettingSection(name: 'Advanced Security'),
_WebEngineHardeningTile(), _WebEngineHardeningTile(),
_FissionEnabledTile(), _FissionEnabledTile(),
_ExtensionsWebAPIEnabledTile(), _ExtensionsWebAPIEnabledTile(),
@@ -812,14 +779,14 @@ Future<void> _showRestartDialog(BuildContext context, WidgetRef ref) async {
} }
} }
class _LocalNetworkAccessSection extends StatelessWidget { class _NetworkProtectionSection extends StatelessWidget {
const _LocalNetworkAccessSection(); const _NetworkProtectionSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Local Network / Device Access Blocking'), SettingSection(name: 'Network Protection'),
_LnaEnabledTile(), _LnaEnabledTile(),
_LnaBlockingTile(), _LnaBlockingTile(),
_LnaBlockTrackersTile(), _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 { class _LnaEnabledTile extends HookConsumerWidget {
const _LnaEnabledTile(); const _LnaEnabledTile();
@@ -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/bang_icon.dart';
import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.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/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/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'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class SearchContentSettingsScreen extends StatelessWidget { class SearchSettingsScreen extends StatelessWidget {
const SearchContentSettingsScreen({super.key}); const SearchSettingsScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Search & Content')), appBar: AppBar(title: const Text('Search')),
body: SafeArea( body: SafeArea(
child: FadingScroll( child: FadingScroll(
fadingSize: 25, fadingSize: 25,
@@ -49,10 +47,9 @@ class SearchContentSettingsScreen extends StatelessWidget {
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [ children: const [
_SearchSection(), _ProvidersSection(),
_ContentViewingSection(), _BangShortcutsSection(),
_ContentEnhancementSection(), _HistorySuggestionsSection(),
_ExtensionsSection(),
], ],
); );
}, },
@@ -62,18 +59,44 @@ class SearchContentSettingsScreen extends StatelessWidget {
} }
} }
class _SearchSection extends StatelessWidget { class _ProvidersSection extends StatelessWidget {
const _SearchSection(); const _ProvidersSection();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Search'), SettingSection(name: 'Providers'),
_DefaultSearchProviderSection(), _DefaultSearchProviderSection(),
_CustomSearchEnginesTile(),
_BangsTile(),
_AutocompleteProviderSection(), _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(), _MaxSearchHistoryEntriesSection(),
_AllowClipboardAccessTile(), _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 { class _DefaultSearchProviderSection extends StatelessWidget {
const _DefaultSearchProviderSection(); const _DefaultSearchProviderSection();
@@ -243,8 +222,8 @@ class _BangsTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: const Text('Bangs'), title: const Text('Bang Settings'),
subtitle: const Text('Search shortcuts management'), subtitle: const Text('Manage bang repositories and usage data'),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
horizontal: 16.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 { class _AllowClipboardAccessTile extends HookConsumerWidget {
const _AllowClipboardAccessTile(); const _AllowClipboardAccessTile();
@@ -37,12 +37,14 @@ class SettingsScreen extends HookConsumerWidget {
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [ children: const [
_AppearanceDisplayTile(), _GeneralTile(),
_BrowsingTile(),
_ToolbarLayoutTile(),
_WebContentTile(),
_SearchTile(),
_PrivacySecurityTile(), _PrivacySecurityTile(),
_SearchContentTile(), _ExtensionsTile(),
_TabsBehaviorTile(),
_SyncTile(), _SyncTile(),
_FingerprintingTile(),
_AdvancedTile(), _AdvancedTile(),
], ],
); );
@@ -52,8 +54,8 @@ class SettingsScreen extends HookConsumerWidget {
} }
} }
class _AppearanceDisplayTile extends StatelessWidget { class _GeneralTile extends StatelessWidget {
const _AppearanceDisplayTile(); const _GeneralTile();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -61,16 +63,66 @@ class _AppearanceDisplayTile extends StatelessWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
title: const Text('Appearance & Display'), title: const Text('General'),
subtitle: const Text('Theme, tabs, toolbars, gestures'), subtitle: const Text('Appearance, language, downloads'),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
horizontal: 16.0, horizontal: 16.0,
), ),
leading: const Icon(Icons.palette), leading: const Icon(Icons.tune),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () async { 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 { class _WebContentTile extends StatelessWidget {
const _SearchContentTile(); const _WebContentTile();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -111,8 +163,33 @@ class _SearchContentTile extends StatelessWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
title: const Text('Search & Content'), title: const Text('Web Content'),
subtitle: const Text('Search providers, reader mode, AI'), 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( contentPadding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
horizontal: 16.0, horizontal: 16.0,
@@ -120,15 +197,15 @@ class _SearchContentTile extends StatelessWidget {
leading: const Icon(MdiIcons.magnify), leading: const Icon(MdiIcons.magnify),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () async { onTap: () async {
await SearchContentSettingsRoute().push(context); await SearchSettingsRoute().push(context);
}, },
), ),
); );
} }
} }
class _TabsBehaviorTile extends StatelessWidget { class _ExtensionsTile extends StatelessWidget {
const _TabsBehaviorTile(); const _ExtensionsTile();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -136,41 +213,16 @@ class _TabsBehaviorTile extends StatelessWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
title: const Text('Tabs & Behavior'), title: const Text('Extensions'),
subtitle: const Text('New tab defaults, link handling'), subtitle: const Text('Install and manage extension sources'),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
horizontal: 16.0, horizontal: 16.0,
), ),
leading: const Icon(MdiIcons.tab), leading: const Icon(MdiIcons.puzzleOutline),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () async { onTap: () async {
await TabsBehaviorSettingsRoute().push(context); await ExtensionsSettingsRoute().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);
}, },
), ),
); );
@@ -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/browser/presentation/widgets/tabs_action_button.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.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/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/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class TabBarSettingsScreen extends HookConsumerWidget { class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
const TabBarSettingsScreen({super.key}); const ToolbarLayoutSettingsScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(generalSettingsWithDefaultsProvider); final settings = ref.watch(generalSettingsWithDefaultsProvider);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Tab Bar')), appBar: AppBar(title: const Text('Toolbar & Layout')),
body: SafeArea( body: SafeArea(
child: FadingScroll( child: FadingScroll(
fadingSize: 25, fadingSize: 25,
@@ -58,7 +59,7 @@ class TabBarSettingsScreen extends HookConsumerWidget {
), ),
const SliverPadding( const SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 12.0), 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 { class _ToolbarLayoutContent extends StatelessWidget {
const _TabBarLayoutSection(); const _ToolbarLayoutContent();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column( return const Column(
children: [ children: [
SettingSection(name: 'Tab Bar'),
_TabBarPositionSection(), _TabBarPositionSection(),
_TabBarLayoutModeSection(), _TabBarLayoutModeSection(),
_AutoHideTabBarTile(),
SettingSection(name: 'Contextual Toolbar'),
_ShowContextualTabBarTile(), _ShowContextualTabBarTile(),
_CustomizeToolbarButtonsTile(), _CustomizeToolbarButtonsTile(),
_AutoHideTabBarTile(), SettingSection(name: 'Quick Tab Switcher'),
_ShowQuickTabSwitcherBarTile(), _ShowQuickTabSwitcherBarTile(),
_QuickTabSwitcherModeSection(), _QuickTabSwitcherModeSection(),
_QuickTabSwitcherHistorySuggestionsTile(), _QuickTabSwitcherHistorySuggestionsTile(),
_QuickTabSwitcherShowTitlesTile(), _QuickTabSwitcherShowTitlesTile(),
SettingSection(name: 'Tab View'),
_BottomSheetTabViewTile(),
_TabListShowFaviconsTile(),
], ],
); );
} }
@@ -93,7 +100,7 @@ class _TabBarLayoutSection extends StatelessWidget {
class _TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate { class _TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate {
const _TabBarPreviewHeaderDelegate({required this.settings}); const _TabBarPreviewHeaderDelegate({required this.settings});
static const _kPreviewBaseHeight = 160.0; static const _kPreviewBaseHeight = 164.0;
final GeneralSettings settings; final GeneralSettings settings;
@@ -321,7 +328,9 @@ class _TabBarPreviewCard extends HookWidget {
children: [ children: [
const ListTile( const ListTile(
title: Text('Live Preview'), 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), leading: Icon(MdiIcons.televisionGuide),
contentPadding: EdgeInsets.symmetric(horizontal: 8.0), contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
), ),
@@ -596,7 +605,7 @@ class _ShowContextualTabBarTile extends HookConsumerWidget {
); );
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
title: const Text('Show Contextual Tab Bar'), title: const Text('Show Contextual Toolbar'),
subtitle: const Text( subtitle: const Text(
'Show additional bottom toolbar for navigation and actions', '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),
);
},
);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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),
);
},
);
}
}