reorganize settings

This commit is contained in:
Fabian Freund
2026-01-13 18:05:54 +01:00
parent 618ed57f45
commit 33e3fdd111
12 changed files with 1990 additions and 1283 deletions
+6 -3
View File
@@ -53,16 +53,19 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/c
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart';
import 'package:weblibre/features/onboarding/presentation/onboarding.dart';
import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart';
import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/appearance_display_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/developer_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/general_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/fingerprinting_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/search_content_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
import 'package:weblibre/features/settings/presentation/screens/web_engine_settings.dart';
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
+167 -96
View File
@@ -80,9 +80,34 @@ RouteBase get $settingsRoute => GoRouteData.$route(
factory: $SettingsRoute._fromState,
routes: [
GoRouteData.$route(
path: 'general',
name: 'GeneralSettingsRoute',
factory: $GeneralSettingsRoute._fromState,
path: 'appearance_display',
name: 'AppearanceDisplaySettingsRoute',
factory: $AppearanceDisplaySettingsRoute._fromState,
),
GoRouteData.$route(
path: 'privacy_security',
name: 'PrivacySecuritySettingsRoute',
factory: $PrivacySecuritySettingsRoute._fromState,
),
GoRouteData.$route(
path: 'search_content',
name: 'SearchContentSettingsRoute',
factory: $SearchContentSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'tabs_behavior',
name: 'TabsBehaviorSettingsRoute',
factory: $TabsBehaviorSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'fingerprinting',
name: 'FingerprintingSettingsRoute',
factory: $FingerprintingSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'advanced',
name: 'AdvancedSettingsRoute',
factory: $AdvancedSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'bang',
@@ -90,50 +115,36 @@ RouteBase get $settingsRoute => GoRouteData.$route(
factory: $BangSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'web_engine',
name: 'WebEngineSettingsRoute',
factory: $WebEngineSettingsRoute._fromState,
path: 'hardening',
name: 'WebEngineHardeningRoute',
factory: $WebEngineHardeningRoute._fromState,
routes: [
GoRouteData.$route(
path: 'hardening',
name: 'WebEngineHardeningRoute',
factory: $WebEngineHardeningRoute._fromState,
routes: [
GoRouteData.$route(
path: 'group/:group',
name: 'WebEngineHardeningGroupRoute',
factory: $WebEngineHardeningGroupRoute._fromState,
),
],
),
GoRouteData.$route(
path: 'doh',
name: 'DohSettingsRoute',
factory: $DohSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'fingerprint',
name: 'FingerprintSettingsRoute',
factory: $FingerprintSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'locales',
name: 'LocaleSettingsRoute',
factory: $LocaleSettingsRoute._fromState,
path: 'group/:group',
name: 'WebEngineHardeningGroupRoute',
factory: $WebEngineHardeningGroupRoute._fromState,
),
],
),
GoRouteData.$route(
path: 'developer',
name: 'DeveloperSettingsRoute',
factory: $DeveloperSettingsRoute._fromState,
routes: [
GoRouteData.$route(
path: 'addon_collection',
name: 'AddonCollectionRoute',
factory: $AddonCollectionRoute._fromState,
),
],
path: 'doh',
name: 'DohSettingsRoute',
factory: $DohSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'fingerprint',
name: 'FingerprintSettingsRoute',
factory: $FingerprintSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'locales',
name: 'LocaleSettingsRoute',
factory: $LocaleSettingsRoute._fromState,
),
GoRouteData.$route(
path: 'addon_collection',
name: 'AddonCollectionRoute',
factory: $AddonCollectionRoute._fromState,
),
],
);
@@ -158,12 +169,117 @@ mixin $SettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location);
}
mixin $GeneralSettingsRoute on GoRouteData {
static GeneralSettingsRoute _fromState(GoRouterState state) =>
GeneralSettingsRoute();
mixin $AppearanceDisplaySettingsRoute on GoRouteData {
static AppearanceDisplaySettingsRoute _fromState(GoRouterState state) =>
AppearanceDisplaySettingsRoute();
@override
String get location => GoRouteData.$location('/settings/general');
String get location => GoRouteData.$location('/settings/appearance_display');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $PrivacySecuritySettingsRoute on GoRouteData {
static PrivacySecuritySettingsRoute _fromState(GoRouterState state) =>
PrivacySecuritySettingsRoute();
@override
String get location => GoRouteData.$location('/settings/privacy_security');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $SearchContentSettingsRoute on GoRouteData {
static SearchContentSettingsRoute _fromState(GoRouterState state) =>
SearchContentSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/search_content');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $TabsBehaviorSettingsRoute on GoRouteData {
static TabsBehaviorSettingsRoute _fromState(GoRouterState state) =>
TabsBehaviorSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/tabs_behavior');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $FingerprintingSettingsRoute on GoRouteData {
static FingerprintingSettingsRoute _fromState(GoRouterState state) =>
FingerprintingSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/fingerprinting');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $AdvancedSettingsRoute on GoRouteData {
static AdvancedSettingsRoute _fromState(GoRouterState state) =>
AdvancedSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/advanced');
@override
void go(BuildContext context) => context.go(location);
@@ -200,34 +316,12 @@ mixin $BangSettingsRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location);
}
mixin $WebEngineSettingsRoute on GoRouteData {
static WebEngineSettingsRoute _fromState(GoRouterState state) =>
WebEngineSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/web_engine');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $WebEngineHardeningRoute on GoRouteData {
static WebEngineHardeningRoute _fromState(GoRouterState state) =>
WebEngineHardeningRoute();
@override
String get location =>
GoRouteData.$location('/settings/web_engine/hardening');
String get location => GoRouteData.$location('/settings/hardening');
@override
void go(BuildContext context) => context.go(location);
@@ -252,7 +346,7 @@ mixin $WebEngineHardeningGroupRoute on GoRouteData {
@override
String get location => GoRouteData.$location(
'/settings/web_engine/hardening/group/${Uri.encodeComponent(_self.group)}',
'/settings/hardening/group/${Uri.encodeComponent(_self.group)}',
);
@override
@@ -273,7 +367,7 @@ mixin $DohSettingsRoute on GoRouteData {
static DohSettingsRoute _fromState(GoRouterState state) => DohSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/web_engine/doh');
String get location => GoRouteData.$location('/settings/doh');
@override
void go(BuildContext context) => context.go(location);
@@ -294,8 +388,7 @@ mixin $FingerprintSettingsRoute on GoRouteData {
FingerprintSettingsRoute();
@override
String get location =>
GoRouteData.$location('/settings/web_engine/fingerprint');
String get location => GoRouteData.$location('/settings/fingerprint');
@override
void go(BuildContext context) => context.go(location);
@@ -316,28 +409,7 @@ mixin $LocaleSettingsRoute on GoRouteData {
LocaleSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/web_engine/locales');
@override
void go(BuildContext context) => context.go(location);
@override
Future<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 $DeveloperSettingsRoute on GoRouteData {
static DeveloperSettingsRoute _fromState(GoRouterState state) =>
DeveloperSettingsRoute();
@override
String get location => GoRouteData.$location('/settings/developer');
String get location => GoRouteData.$location('/settings/locales');
@override
void go(BuildContext context) => context.go(location);
@@ -358,8 +430,7 @@ mixin $AddonCollectionRoute on GoRouteData {
AddonCollectionRoute();
@override
String get location =>
GoRouteData.$location('/settings/developer/addon_collection');
String get location => GoRouteData.$location('/settings/addon_collection');
@override
void go(BuildContext context) => context.go(location);
+83 -49
View File
@@ -23,45 +23,53 @@ part of 'routes.dart';
name: 'SettingsRoute',
path: '/settings',
routes: [
TypedGoRoute<GeneralSettingsRoute>(
name: 'GeneralSettingsRoute',
path: 'general',
TypedGoRoute<AppearanceDisplaySettingsRoute>(
name: 'AppearanceDisplaySettingsRoute',
path: 'appearance_display',
),
TypedGoRoute<PrivacySecuritySettingsRoute>(
name: 'PrivacySecuritySettingsRoute',
path: 'privacy_security',
),
TypedGoRoute<SearchContentSettingsRoute>(
name: 'SearchContentSettingsRoute',
path: 'search_content',
),
TypedGoRoute<TabsBehaviorSettingsRoute>(
name: 'TabsBehaviorSettingsRoute',
path: 'tabs_behavior',
),
TypedGoRoute<FingerprintingSettingsRoute>(
name: 'FingerprintingSettingsRoute',
path: 'fingerprinting',
),
TypedGoRoute<AdvancedSettingsRoute>(
name: 'AdvancedSettingsRoute',
path: 'advanced',
),
TypedGoRoute<BangSettingsRoute>(name: 'BangSettingsRoute', path: 'bang'),
TypedGoRoute<WebEngineSettingsRoute>(
name: 'WebEngineSettingsRoute',
path: 'web_engine',
TypedGoRoute<WebEngineHardeningRoute>(
name: 'WebEngineHardeningRoute',
path: 'hardening',
routes: [
TypedGoRoute<WebEngineHardeningRoute>(
name: 'WebEngineHardeningRoute',
path: 'hardening',
routes: [
TypedGoRoute<WebEngineHardeningGroupRoute>(
name: 'WebEngineHardeningGroupRoute',
path: 'group/:group',
),
],
),
TypedGoRoute<DohSettingsRoute>(name: 'DohSettingsRoute', path: 'doh'),
TypedGoRoute<FingerprintSettingsRoute>(
name: 'FingerprintSettingsRoute',
path: 'fingerprint',
),
TypedGoRoute<LocaleSettingsRoute>(
name: 'LocaleSettingsRoute',
path: 'locales',
TypedGoRoute<WebEngineHardeningGroupRoute>(
name: 'WebEngineHardeningGroupRoute',
path: 'group/:group',
),
],
),
TypedGoRoute<DeveloperSettingsRoute>(
name: 'DeveloperSettingsRoute',
path: 'developer',
routes: [
TypedGoRoute<AddonCollectionRoute>(
name: 'AddonCollectionRoute',
path: 'addon_collection',
),
],
TypedGoRoute<DohSettingsRoute>(name: 'DohSettingsRoute', path: 'doh'),
TypedGoRoute<FingerprintSettingsRoute>(
name: 'FingerprintSettingsRoute',
path: 'fingerprint',
),
TypedGoRoute<LocaleSettingsRoute>(
name: 'LocaleSettingsRoute',
path: 'locales',
),
TypedGoRoute<AddonCollectionRoute>(
name: 'AddonCollectionRoute',
path: 'addon_collection',
),
],
)
@@ -72,10 +80,50 @@ class SettingsRoute extends GoRouteData with $SettingsRoute {
}
}
class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute {
class AppearanceDisplaySettingsRoute extends GoRouteData
with $AppearanceDisplaySettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const GeneralSettingsScreen();
return const AppearanceDisplaySettingsScreen();
}
}
class PrivacySecuritySettingsRoute extends GoRouteData
with $PrivacySecuritySettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const PrivacySecuritySettingsScreen();
}
}
class SearchContentSettingsRoute extends GoRouteData
with $SearchContentSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const SearchContentSettingsScreen();
}
}
class TabsBehaviorSettingsRoute extends GoRouteData
with $TabsBehaviorSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const TabsBehaviorSettingsScreen();
}
}
class FingerprintingSettingsRoute extends GoRouteData
with $FingerprintingSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const FingerprintingSettingsScreen();
}
}
class AdvancedSettingsRoute extends GoRouteData with $AdvancedSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const AdvancedSettingsScreen();
}
}
@@ -108,20 +156,6 @@ class LocaleSettingsRoute extends GoRouteData with $LocaleSettingsRoute {
}
}
class WebEngineSettingsRoute extends GoRouteData with $WebEngineSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const WebEngineSettingsScreen();
}
}
class DeveloperSettingsRoute extends GoRouteData with $DeveloperSettingsRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
return const DeveloperSettingsScreen();
}
}
class AddonCollectionRoute extends GoRouteData with $AddonCollectionRoute {
@override
Widget build(BuildContext context, GoRouterState state) {
@@ -25,45 +25,90 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/core/providers/app_state.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/dialogs/user_agent_restart_dialog.dart';
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/cache.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/utils/exit_app.dart';
import 'package:weblibre/utils/ui_helper.dart';
class DeveloperSettingsScreen extends StatelessWidget {
const DeveloperSettingsScreen();
class AdvancedSettingsScreen extends StatelessWidget {
const AdvancedSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Developer Settings')),
body: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
appBar: AppBar(title: const Text('Advanced')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_JavaScriptTile(),
_UserAgentTile(),
_EnterpriseRootsTile(),
_FingerprintProtectionTile(),
_ErrorLogsTile(),
_DartVmTile(),
_AddonCollectionTile(),
_ResetUITile(),
_ContentBehaviorSection(),
_StorageDebuggingSection(),
_ResetSection(),
],
);
},
),
),
);
}
}
class _ContentBehaviorSection extends StatelessWidget {
const _ContentBehaviorSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Content Behavior'),
_JavaScriptTile(),
_UserAgentTile(),
_EnterpriseRootsTile(),
],
);
}
}
class _StorageDebuggingSection extends StatelessWidget {
const _StorageDebuggingSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Storage & Debugging'),
_IconCacheTile(),
_ErrorLogsTile(),
_DartVmTile(),
],
);
}
}
class _ResetSection extends StatelessWidget {
const _ResetSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Reset'),
_ResetUITile(),
],
);
}
}
@@ -172,22 +217,52 @@ class _EnterpriseRootsTile extends HookConsumerWidget {
}
}
class _FingerprintProtectionTile extends StatelessWidget {
const _FingerprintProtectionTile();
class _IconCacheTile extends HookConsumerWidget {
const _IconCacheTile();
@override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Fingerprint Protection'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
Widget build(BuildContext context, WidgetRef ref) {
final size = ref.watch(
iconCacheSizeMegabytesProvider.select((value) => value.value),
);
return CustomListTile(
title: 'Icon Cache',
subtitle: 'Stored favicons',
prefix: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(
Icons.image,
size: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(
textStyle: DefaultTextStyle.of(context).style,
),
child: Table(
columnWidths: const {0: FixedColumnWidth(100)},
children: [
TableRow(
children: [
const Text('Size'),
Text('${size?.toStringAsFixed(2) ?? 0} MB'),
],
),
],
),
),
),
suffix: FilledButton.icon(
onPressed: () async {
await ref.read(cacheRepositoryProvider.notifier).clearCache();
},
icon: const Icon(Icons.delete),
label: const Text('Clear'),
),
leading: const Icon(MdiIcons.fingerprint),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await FingerprintSettingsRoute().push(context);
},
);
}
}
@@ -268,25 +343,6 @@ class _DartVmTile extends StatelessWidget {
}
}
class _AddonCollectionTile extends StatelessWidget {
const _AddonCollectionTile();
@override
Widget build(BuildContext context) {
return ListTile(
leading: const Icon(MdiIcons.puzzle),
title: const Text('Custom Extension Collection'),
subtitle: const Text(
'Custom Add-on Collections are curated lists of extensions that users can create and share.',
),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await AddonCollectionRoute().push(context);
},
);
}
}
class _ResetUITile extends ConsumerWidget {
const _ResetUITile();
@@ -0,0 +1,451 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class AppearanceDisplaySettingsScreen extends StatelessWidget {
const AppearanceDisplaySettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Appearance & Display')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_VisualSection(),
_TabBarLayoutSection(),
_GesturesSection(),
],
);
},
),
),
);
}
}
class _VisualSection extends StatelessWidget {
const _VisualSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Visual'),
_ThemeSection(),
],
);
}
}
class _TabBarLayoutSection extends StatelessWidget {
const _TabBarLayoutSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tab Bar Layout'),
_TabBarPositionSection(),
_ShowContextualTabBarTile(),
_AutoHideTabBarTile(),
_BottomSheetTabViewTile(),
_ShowQuickTabSwitcherBarTile(),
_QuickTabSwitcherModeSection(),
],
);
}
}
class _GesturesSection extends StatelessWidget {
const _GesturesSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Gestures'),
_PullToRefreshTile(),
_DoubleBackCloseTabTile(),
],
);
}
}
class _ThemeSection extends HookConsumerWidget {
const _ThemeSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.themeMode),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Theme'),
leading: Icon(Icons.palette),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton<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 _TabBarPositionSection extends HookConsumerWidget {
const _TabBarPositionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarPosition = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarPosition),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Tab Bar Position'),
leading: Icon(MdiIcons.dockWindow),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: tabBarPosition,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarPosition(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: TabBarPosition.top,
title: Text('Top'),
subtitle: Text(
'Persistent tab bar without auto-hide',
),
),
RadioListTile.adaptive(
value: TabBarPosition.bottom,
title: Text('Bottom'),
subtitle: Text('Tab bar with auto-hide support'),
),
],
),
),
],
),
);
}
}
class _ShowContextualTabBarTile extends HookConsumerWidget {
const _ShowContextualTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarShowContextualBar = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.tabBarShowContextualBar),
);
return SwitchListTile.adaptive(
title: const Text('Show Contextual Tab Bar'),
subtitle: const Text(
'Show additional bottom toolbar for navigation and actions',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabBarShowContextualBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarShowContextualBar(value),
);
},
);
}
}
class _ShowQuickTabSwitcherBarTile extends HookConsumerWidget {
const _ShowQuickTabSwitcherBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarShowQuickTabSwitcherBar = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.tabBarShowQuickTabSwitcherBar),
);
return SwitchListTile.adaptive(
title: const Text('Show Quick Tab Switcher Bar'),
subtitle: const Text(
'Show additional toolbar to quickly switch to recently used tabs',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabBarShowQuickTabSwitcherBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.tabBarShowQuickTabSwitcherBar(value),
);
},
);
}
}
class _QuickTabSwitcherModeSection extends HookConsumerWidget {
const _QuickTabSwitcherModeSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final quickTabSwitcherMode = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.quickTabSwitcherMode),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Quick Tab Switcher Mode'),
leading: Icon(MdiIcons.folderSettings),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: quickTabSwitcherMode,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.quickTabSwitcherMode(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: QuickTabSwitcherMode.lastUsedTabs,
title: Text('Recently Used Tabs'),
subtitle: Text(
'Recently used tabs across all containers',
),
),
RadioListTile.adaptive(
value: QuickTabSwitcherMode.containerTabs,
title: Text('Container Tabs'),
subtitle: Text(
'Ordered tabs of the selected container',
),
),
],
),
),
],
),
);
}
}
class _AutoHideTabBarTile extends HookConsumerWidget {
const _AutoHideTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.autoHideTabBar),
);
return SwitchListTile.adaptive(
title: const Text('Auto Hide Tab Bar'),
subtitle: const Text('Hide tab bar when scrolling'),
secondary: const Icon(MdiIcons.folderHidden),
value: autoHideTabBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.autoHideTabBar(value),
);
},
);
}
}
class _BottomSheetTabViewTile extends HookConsumerWidget {
const _BottomSheetTabViewTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabViewBottomSheet = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabViewBottomSheet),
);
return SwitchListTile.adaptive(
title: const Text('Bottom Sheet Tab View'),
subtitle: const Text(
'Display tabs in a bottom sheet instead of fullscreen',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabViewBottomSheet,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabViewBottomSheet(value),
);
},
);
}
}
class _PullToRefreshTile extends HookConsumerWidget {
const _PullToRefreshTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final pullToRefreshEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled),
);
return SwitchListTile.adaptive(
title: const Text('Pull to Refresh'),
subtitle: const Text('Swipe down on pages to reload them'),
secondary: const Icon(MdiIcons.gestureSwipeDown),
value: pullToRefreshEnabled,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.pullToRefreshEnabled(value),
);
},
);
}
}
class _DoubleBackCloseTabTile extends HookConsumerWidget {
const _DoubleBackCloseTabTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final doubleBackCloseTab = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.doubleBackCloseTab),
);
return SwitchListTile.adaptive(
title: const Text('Double Back to Close Tab'),
subtitle: const Text(
'When enabled, press back twice to close the tab. When disabled, back button only navigates page history.',
),
secondary: const Icon(MdiIcons.gestureDoubleTap),
value: doubleBackCloseTab,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.doubleBackCloseTab(value),
);
},
);
}
}
@@ -0,0 +1,162 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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);
},
);
}
}
@@ -1,949 +0,0 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/cache.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class GeneralSettingsScreen extends StatelessWidget {
const GeneralSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('General Settings')),
body: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
children: const [
_ThemeSection(),
_NewTabDefaultSection(),
_ExternalLinkHandlingSection(),
_DefaultSearchProviderSection(),
_AutocompleteProviderSection(),
_OnDeviceAiTile(),
_EnableReaderModeTile(),
_EnforceReaderModeTile(),
_ReaderModeInTabBarTile(),
_TabBarPositionSection(),
_ShowContextualTabBarTile(),
_ShowQuickTabSwitcherBarTile(),
_QuickTabSwitcherModeSection(),
_CreateChildTabsTile(),
_ShowExtensionShortcutTile(),
_AutoHideTabBarTile(),
_BottomSheetTabViewTile(),
_TabBarSwipeBehaviorSection(),
_PullToRefreshTile(),
_DoubleBackCloseTabTile(),
_IconCacheTile(),
],
);
},
),
);
}
}
class _ThemeSection extends HookConsumerWidget {
const _ThemeSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.themeMode),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Theme'),
leading: Icon(Icons.palette),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton<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 _NewTabDefaultSection extends HookConsumerWidget {
const _NewTabDefaultSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final defaultCreateTabType = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.defaultCreateTabType),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('New Tab Default'),
subtitle: Text(
'Choose the default type for manually created tabs',
),
leading: Icon(MdiIcons.tab),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: TabType.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
ButtonSegment(
value: TabType.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
],
selected: {defaultCreateTabType},
onSelectionChanged: (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.defaultCreateTabType(value.first),
);
},
style: switch (defaultCreateTabType) {
TabType.regular => null,
TabType.private => SegmentedButton.styleFrom(
selectedBackgroundColor: appColors.privateSelectionOverlay,
),
TabType.child => null,
},
),
),
],
),
);
}
}
class _ExternalLinkHandlingSection extends HookConsumerWidget {
const _ExternalLinkHandlingSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final tabIntentOpenSetting = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabIntentOpenSetting),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('External Link Handling'),
subtitle: Text(
'Choose how external links open in WebLibre',
),
leading: Icon(MdiIcons.tabPlus),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: TabIntentOpenSetting.ask,
label: Text('Prompt'),
icon: Icon(MdiIcons.messageQuestion),
),
ButtonSegment(
value: TabIntentOpenSetting.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
ButtonSegment(
value: TabIntentOpenSetting.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
],
selected: {tabIntentOpenSetting},
onSelectionChanged: (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.tabIntentOpenSetting(value.first),
);
},
style: switch (tabIntentOpenSetting) {
TabIntentOpenSetting.regular => null,
TabIntentOpenSetting.private => SegmentedButton.styleFrom(
selectedBackgroundColor: appColors.privateSelectionOverlay,
),
TabIntentOpenSetting.ask => null,
},
),
),
],
),
);
}
}
class _DefaultSearchProviderSection extends StatelessWidget {
const _DefaultSearchProviderSection();
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('Default Search Provider'),
leading: Icon(MdiIcons.cloudSearch),
contentPadding: EdgeInsets.zero,
),
Padding(
padding: EdgeInsets.only(left: 40),
child: DefaultSearchSelector(),
),
],
),
);
}
}
class _AutocompleteProviderSection extends HookConsumerWidget {
const _AutocompleteProviderSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final defaultSearchSuggestionsProvider = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.defaultSearchSuggestionsProvider),
);
final relatedBang = defaultSearchSuggestionsProvider.relatedBang;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Default Autocomplete Provider'),
leading: Icon(MdiIcons.weatherCloudyArrowRight),
contentPadding: EdgeInsets.zero,
),
Padding(
padding: const EdgeInsets.only(left: 40),
child: DropdownMenu<SearchSuggestionProviders>(
initialSelection: defaultSearchSuggestionsProvider,
inputDecorationTheme: InputDecorationTheme(
prefixIconConstraints: BoxConstraints.tight(
const Size.square(24),
),
),
width: double.infinity,
leadingIcon:
relatedBang.mapNotNull((trigger) => BangIcon(trigger: trigger)),
dropdownMenuEntries: SearchSuggestionProviders.values
.map((provider) {
return DropdownMenuEntry(
value: provider,
label: provider.label,
leadingIcon: provider.relatedBang.mapNotNull(
(trigger) => BangIcon(trigger: trigger),
),
);
})
.toList(),
onSelected: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.defaultSearchSuggestionsProvider(value),
);
}
},
),
),
],
),
);
}
}
class _OnDeviceAiTile extends HookConsumerWidget {
const _OnDeviceAiTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableLocalAiFeatures = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableLocalAiFeatures),
);
return SwitchListTile.adaptive(
title: const Text('On Device AI'),
subtitle: const Text(
'Local on-device features including container topic and tab suggestions',
),
secondary: const Icon(MdiIcons.creation),
value: enableLocalAiFeatures,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.enableLocalAiFeatures(value),
);
},
);
}
}
class _EnableReaderModeTile extends HookConsumerWidget {
const _EnableReaderModeTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableReadability),
);
return SwitchListTile.adaptive(
title: const Text('Enable Reader Mode'),
subtitle: const Text(
'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.',
),
secondary: const Icon(MdiIcons.bookOpen),
value: enableReadability,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.enableReadability(value),
);
},
);
}
}
class _EnforceReaderModeTile extends HookConsumerWidget {
const _EnforceReaderModeTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableReadability),
);
final enforceReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enforceReadability),
);
return SwitchListTile.adaptive(
title: const Text('Enforce Reader Mode'),
subtitle: const Text(
'Override readability probability of websites and always show Reader Mode capabilities even the site might not be compatible.',
),
secondary: const Icon(MdiIcons.bookCheck),
value: enableReadability && enforceReadability,
onChanged: enableReadability
? (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.enforceReadability(value),
);
}
: null,
);
}
}
class _ReaderModeInTabBarTile extends HookConsumerWidget {
const _ReaderModeInTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarReaderView = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarReaderView),
);
return SwitchListTile.adaptive(
title: const Text('Reader Mode in Tab Bar'),
subtitle: const Text(
'Show reader mode button in the tab bar instead of only in the tab menu',
),
secondary: const Icon(MdiIcons.bookHeart),
value: tabBarReaderView,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarReaderView(value),
);
},
);
}
}
class _TabBarPositionSection extends HookConsumerWidget {
const _TabBarPositionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarPosition = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarPosition),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Tab Bar Position'),
leading: Icon(MdiIcons.dockWindow),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: tabBarPosition,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarPosition(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: TabBarPosition.top,
title: Text('Top'),
subtitle: Text(
'Persistent tab bar without auto-hide',
),
),
RadioListTile.adaptive(
value: TabBarPosition.bottom,
title: Text('Bottom'),
subtitle: Text('Tab bar with auto-hide support'),
),
],
),
),
],
),
);
}
}
class _ShowContextualTabBarTile extends HookConsumerWidget {
const _ShowContextualTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarShowContextualBar = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.tabBarShowContextualBar),
);
return SwitchListTile.adaptive(
title: const Text('Show Contextual Tab Bar'),
subtitle: const Text(
'Show additional bottom toolbar for navigation and actions',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabBarShowContextualBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarShowContextualBar(value),
);
},
);
}
}
class _ShowQuickTabSwitcherBarTile extends HookConsumerWidget {
const _ShowQuickTabSwitcherBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarShowQuickTabSwitcherBar = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.tabBarShowQuickTabSwitcherBar),
);
return SwitchListTile.adaptive(
title: const Text('Show Quick Tab Switcher Bar'),
subtitle: const Text(
'Show additional toolbar to quickly switch to recently used tabs',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabBarShowQuickTabSwitcherBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.tabBarShowQuickTabSwitcherBar(value),
);
},
);
}
}
class _QuickTabSwitcherModeSection extends HookConsumerWidget {
const _QuickTabSwitcherModeSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final quickTabSwitcherMode = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.quickTabSwitcherMode),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Quick Tab Switcher Mode'),
leading: Icon(MdiIcons.folderSettings),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: quickTabSwitcherMode,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.quickTabSwitcherMode(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: QuickTabSwitcherMode.lastUsedTabs,
title: Text('Recently Used Tabs'),
subtitle: Text(
'Recently used tabs across all containers',
),
),
RadioListTile.adaptive(
value: QuickTabSwitcherMode.containerTabs,
title: Text('Container Tabs'),
subtitle: Text(
'Ordered tabs of the selected container',
),
),
],
),
),
],
),
);
}
}
class _CreateChildTabsTile extends HookConsumerWidget {
const _CreateChildTabsTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final createChildTabsOption = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.createChildTabsOption),
);
return SwitchListTile.adaptive(
title: const Text('Create Child Tabs'),
subtitle: const Text(
'Display a button to create a child tab under the current tab (tree view only)',
),
secondary: const Icon(MdiIcons.fileTree),
value: createChildTabsOption,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.createChildTabsOption(value),
);
},
);
}
}
class _ShowExtensionShortcutTile extends HookConsumerWidget {
const _ShowExtensionShortcutTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final showExtensionShortcut = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showExtensionShortcut),
);
return SwitchListTile.adaptive(
title: const Text('Show Extension Shortcut'),
subtitle: const Text(
'Display an extension menu directly on the tab bar',
),
secondary: const Icon(MdiIcons.puzzleHeart),
value: showExtensionShortcut,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.showExtensionShortcut(value),
);
},
);
}
}
class _AutoHideTabBarTile extends HookConsumerWidget {
const _AutoHideTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoHideTabBar = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.autoHideTabBar),
);
return SwitchListTile.adaptive(
title: const Text('Auto Hide Tab Bar'),
subtitle: const Text('Hide tab bar when scrolling'),
secondary: const Icon(MdiIcons.folderHidden),
value: autoHideTabBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.autoHideTabBar(value),
);
},
);
}
}
class _BottomSheetTabViewTile extends HookConsumerWidget {
const _BottomSheetTabViewTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabViewBottomSheet = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabViewBottomSheet),
);
return SwitchListTile.adaptive(
title: const Text('Bottom Sheet Tab View'),
subtitle: const Text(
'Display tabs in a bottom sheet instead of fullscreen',
),
secondary: const Icon(MdiIcons.dockBottom),
value: tabViewBottomSheet,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabViewBottomSheet(value),
);
},
);
}
}
class _TabBarSwipeBehaviorSection extends HookConsumerWidget {
const _TabBarSwipeBehaviorSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarSwipeAction = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarSwipeAction),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Tab Bar Swipe Behavior'),
leading: Icon(MdiIcons.gestureSwipeHorizontal),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: tabBarSwipeAction,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarSwipeAction(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: TabBarSwipeAction.switchLastOpened,
title: Text('Switch to Last Used Tab'),
subtitle: Text(
'Swipe to toggle between current and previously opened tab',
),
),
RadioListTile.adaptive(
value: TabBarSwipeAction.navigateOrderedTabs,
title: Text('Navigate Sequential Tabs'),
subtitle: Text(
'Swipe left/right to move through tabs in order',
),
),
],
),
),
],
),
);
}
}
class _PullToRefreshTile extends HookConsumerWidget {
const _PullToRefreshTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final pullToRefreshEnabled = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.pullToRefreshEnabled),
);
return SwitchListTile.adaptive(
title: const Text('Pull to Refresh'),
subtitle: const Text('Swipe down on pages to reload them'),
secondary: const Icon(MdiIcons.gestureSwipeDown),
value: pullToRefreshEnabled,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.pullToRefreshEnabled(value),
);
},
);
}
}
class _DoubleBackCloseTabTile extends HookConsumerWidget {
const _DoubleBackCloseTabTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final doubleBackCloseTab = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.doubleBackCloseTab),
);
return SwitchListTile.adaptive(
title: const Text('Double Back to Close Tab'),
subtitle: const Text(
'When enabled, press back twice to close the tab. When disabled, back button only navigates page history.',
),
secondary: const Icon(MdiIcons.gestureDoubleTap),
value: doubleBackCloseTab,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.doubleBackCloseTab(value),
);
},
);
}
}
class _IconCacheTile extends HookConsumerWidget {
const _IconCacheTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final size = ref.watch(
iconCacheSizeMegabytesProvider.select((value) => value.value),
);
return CustomListTile(
title: 'Icon Cache',
subtitle: 'Stored favicons',
prefix: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(
Icons.image,
size: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(
textStyle: DefaultTextStyle.of(context).style,
),
child: Table(
columnWidths: const {0: FixedColumnWidth(100)},
children: [
TableRow(
children: [
const Text('Size'),
Text('${size?.toStringAsFixed(2) ?? 0} MB'),
],
),
],
),
),
),
suffix: FilledButton.icon(
onPressed: () async {
await ref.read(cacheRepositoryProvider.notifier).clearCache();
},
icon: const Icon(Icons.delete),
label: const Text('Clear'),
),
);
}
}
@@ -26,41 +26,112 @@ import 'package:nullability/nullability.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class WebEngineSettingsScreen extends StatelessWidget {
const WebEngineSettingsScreen();
class PrivacySecuritySettingsScreen extends StatelessWidget {
const PrivacySecuritySettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Web Engine Settings')),
body: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
appBar: AppBar(title: const Text('Privacy & Security')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_IncognitoModeSection(),
_DeleteBrowsingDataTile(),
_AutoClearHistorySection(),
_BrowserLanguagesTile(),
_GlobalPrivacyControlTile(),
_HttpsOnlyModeSection(),
_DnsTile(),
_PrivacyModesSection(),
_TrackingProtectionSection(),
_BounceTrackingProtectionTile(),
_QueryParameterStrippingSection(),
_PdfViewerTile(),
_WebEngineHardeningTile(),
_ConnectionSecuritySection(),
_DataManagementSection(),
_AdvancedSection(),
],
);
},
),
),
);
}
}
class _PrivacyModesSection extends StatelessWidget {
const _PrivacyModesSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Privacy Modes'),
_IncognitoModeSection(),
_GlobalPrivacyControlTile(),
],
);
}
}
class _TrackingProtectionSection extends StatelessWidget {
const _TrackingProtectionSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tracking Protection'),
_EnhancedTrackingProtectionSection(),
_BounceTrackingProtectionTile(),
_QueryParameterStrippingSection(),
],
);
}
}
class _ConnectionSecuritySection extends StatelessWidget {
const _ConnectionSecuritySection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Connection Security'),
_HttpsOnlyModeSection(),
_DnsTile(),
],
);
}
}
class _DataManagementSection extends StatelessWidget {
const _DataManagementSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Data Management'),
_DeleteBrowsingDataTile(),
_AutoClearHistorySection(),
],
);
}
}
class _AdvancedSection extends StatelessWidget {
const _AdvancedSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Advanced'),
_WebEngineHardeningTile(),
],
);
}
}
@@ -270,26 +341,6 @@ class _AutoClearHistorySection extends HookConsumerWidget {
}
}
class _BrowserLanguagesTile extends StatelessWidget {
const _BrowserLanguagesTile();
@override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Browser Languages'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.translate),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await LocaleSettingsRoute().push(context);
},
);
}
}
class _GlobalPrivacyControlTile extends HookConsumerWidget {
const _GlobalPrivacyControlTile();
@@ -395,8 +446,8 @@ class _DnsTile extends StatelessWidget {
}
}
class _TrackingProtectionSection extends HookConsumerWidget {
const _TrackingProtectionSection();
class _EnhancedTrackingProtectionSection extends HookConsumerWidget {
const _EnhancedTrackingProtectionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -558,8 +609,8 @@ class _QueryParameterStrippingSection extends HookConsumerWidget {
saveEngineSettingsControllerProvider.notifier,
)
.save(
(currentSettings) => currentSettings.copyWith
.queryParameterStripping(value.first),
(currentSettings) =>
currentSettings.copyWith.queryParameterStripping(value.first),
);
},
),
@@ -570,33 +621,6 @@ class _QueryParameterStrippingSection extends HookConsumerWidget {
}
}
class _PdfViewerTile extends HookConsumerWidget {
const _PdfViewerTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enablePdfJs = ref.watch(
engineSettingsWithDefaultsProvider.select((s) => s.enablePdfJs),
);
return SwitchListTile.adaptive(
title: const Text('Built-in PDF Viewer'),
subtitle: const Text(
'Open PDF files directly in the browser without downloading',
),
secondary: const Icon(MdiIcons.filePdfBox),
value: enablePdfJs,
onChanged: (value) async {
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith.enablePdfJs(value),
);
},
);
}
}
class _WebEngineHardeningTile extends StatelessWidget {
const _WebEngineHardeningTile();
@@ -0,0 +1,407 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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:nullability/nullability.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class SearchContentSettingsScreen extends StatelessWidget {
const SearchContentSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search & Content')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_SearchSection(),
_ContentViewingSection(),
_ContentEnhancementSection(),
_ExtensionsSection(),
],
);
},
),
),
);
}
}
class _SearchSection extends StatelessWidget {
const _SearchSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Search'),
_DefaultSearchProviderSection(),
_BangsTile(),
_AutocompleteProviderSection(),
],
);
}
}
class _ContentViewingSection extends StatelessWidget {
const _ContentViewingSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Content Viewing'),
_PdfViewerTile(),
_EnableReaderModeTile(),
_ReaderModeInTabBarTile(),
_EnforceReaderModeTile(),
],
);
}
}
class _ContentEnhancementSection extends StatelessWidget {
const _ContentEnhancementSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Content Enhancement'),
_OnDeviceAiTile(),
],
);
}
}
class _ExtensionsSection extends StatelessWidget {
const _ExtensionsSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Extensions'),
_AddonCollectionTile(),
],
);
}
}
class _DefaultSearchProviderSection extends StatelessWidget {
const _DefaultSearchProviderSection();
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('Default Search Provider'),
leading: Icon(MdiIcons.cloudSearch),
contentPadding: EdgeInsets.zero,
),
Padding(
padding: EdgeInsets.only(left: 40),
child: DefaultSearchSelector(),
),
],
),
);
}
}
class _AutocompleteProviderSection extends HookConsumerWidget {
const _AutocompleteProviderSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final defaultSearchSuggestionsProvider = ref.watch(
generalSettingsWithDefaultsProvider
.select((s) => s.defaultSearchSuggestionsProvider),
);
final relatedBang = defaultSearchSuggestionsProvider.relatedBang;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Default Autocomplete Provider'),
leading: Icon(MdiIcons.weatherCloudyArrowRight),
contentPadding: EdgeInsets.zero,
),
Padding(
padding: const EdgeInsets.only(left: 40),
child: DropdownMenu<SearchSuggestionProviders>(
initialSelection: defaultSearchSuggestionsProvider,
inputDecorationTheme: InputDecorationTheme(
prefixIconConstraints: BoxConstraints.tight(
const Size.square(24),
),
),
width: double.infinity,
leadingIcon:
relatedBang.mapNotNull((trigger) => BangIcon(trigger: trigger)),
dropdownMenuEntries: SearchSuggestionProviders.values
.map((provider) {
return DropdownMenuEntry(
value: provider,
label: provider.label,
leadingIcon: provider.relatedBang.mapNotNull(
(trigger) => BangIcon(trigger: trigger),
),
);
})
.toList(),
onSelected: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.defaultSearchSuggestionsProvider(value),
);
}
},
),
),
],
),
);
}
}
class _BangsTile extends StatelessWidget {
const _BangsTile();
@override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Bangs'),
subtitle: const Text('Search shortcuts management'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.exclamationThick),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await BangSettingsRoute().push(context);
},
);
}
}
class _OnDeviceAiTile extends HookConsumerWidget {
const _OnDeviceAiTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableLocalAiFeatures = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableLocalAiFeatures),
);
return SwitchListTile.adaptive(
title: const Text('On Device AI'),
subtitle: const Text(
'Local on-device features including container topic and tab suggestions',
),
secondary: const Icon(MdiIcons.creation),
value: enableLocalAiFeatures,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.enableLocalAiFeatures(value),
);
},
);
}
}
class _EnableReaderModeTile extends HookConsumerWidget {
const _EnableReaderModeTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableReadability),
);
return SwitchListTile.adaptive(
title: const Text('Enable Reader Mode'),
subtitle: const Text(
'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.',
),
secondary: const Icon(MdiIcons.bookOpen),
value: enableReadability,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.enableReadability(value),
);
},
);
}
}
class _EnforceReaderModeTile extends HookConsumerWidget {
const _EnforceReaderModeTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enableReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enableReadability),
);
final enforceReadability = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.enforceReadability),
);
return SwitchListTile.adaptive(
title: const Text('Enforce Reader Mode'),
subtitle: const Text(
'Override readability probability of websites and always show Reader Mode capabilities even the site might not be compatible.',
),
secondary: const Icon(MdiIcons.bookCheck),
value: enableReadability && enforceReadability,
onChanged: enableReadability
? (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.enforceReadability(value),
);
}
: null,
);
}
}
class _ReaderModeInTabBarTile extends HookConsumerWidget {
const _ReaderModeInTabBarTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarReaderView = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarReaderView),
);
return SwitchListTile.adaptive(
title: const Text('Reader Mode in Tab Bar'),
subtitle: const Text(
'Show reader mode button in the tab bar instead of only in the tab menu',
),
secondary: const Icon(MdiIcons.bookHeart),
value: tabBarReaderView,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarReaderView(value),
);
},
);
}
}
class _PdfViewerTile extends HookConsumerWidget {
const _PdfViewerTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enablePdfJs = ref.watch(
engineSettingsWithDefaultsProvider.select((s) => s.enablePdfJs),
);
return SwitchListTile.adaptive(
title: const Text('Built-in PDF Viewer'),
subtitle: const Text(
'Open PDF files directly in the browser without downloading',
),
secondary: const Icon(MdiIcons.filePdfBox),
value: enablePdfJs,
onChanged: (value) async {
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith.enablePdfJs(value),
);
},
);
}
}
class _AddonCollectionTile extends StatelessWidget {
const _AddonCollectionTile();
@override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Custom Extension Collection'),
subtitle: const Text(
'Custom Add-on Collections are curated lists of extensions that users can create and share.',
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.puzzle),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await AddonCollectionRoute().push(context);
},
);
}
}
@@ -10,7 +10,7 @@
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* but WITHOUT ANY WARRANTY; without even the the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
@@ -36,74 +36,13 @@ class SettingsScreen extends HookConsumerWidget {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: [
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('General'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await GeneralSettingsRoute().push(context);
},
),
),
const SizedBox(height: 8),
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Web Engine'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.web),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await WebEngineSettingsRoute().push(context);
},
),
),
const SizedBox(height: 8),
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Bangs'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.exclamationThick),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await BangSettingsRoute().push(context);
},
),
),
const SizedBox(height: 8),
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Developer'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(Icons.developer_mode),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await DeveloperSettingsRoute().push(context);
},
),
),
children: const [
_AppearanceDisplayTile(),
_PrivacySecurityTile(),
_SearchContentTile(),
_TabsBehaviorTile(),
_FingerprintingTile(),
_AdvancedTile(),
],
);
},
@@ -111,3 +50,153 @@ class SettingsScreen extends HookConsumerWidget {
);
}
}
class _AppearanceDisplayTile extends StatelessWidget {
const _AppearanceDisplayTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Appearance & Display'),
subtitle: const Text('Theme, tabs, toolbars, gestures'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(Icons.palette),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await AppearanceDisplaySettingsRoute().push(context);
},
),
);
}
}
class _PrivacySecurityTile extends StatelessWidget {
const _PrivacySecurityTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Privacy & Security'),
subtitle: const Text('Tracking protection, data clearing'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.shieldLock),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await PrivacySecuritySettingsRoute().push(context);
},
),
);
}
}
class _SearchContentTile extends StatelessWidget {
const _SearchContentTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Search & Content'),
subtitle: const Text('Search providers, reader mode, AI'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.magnify),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await SearchContentSettingsRoute().push(context);
},
),
);
}
}
class _TabsBehaviorTile extends StatelessWidget {
const _TabsBehaviorTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Tabs & Behavior'),
subtitle: const Text('New tab defaults, link handling'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.tab),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await TabsBehaviorSettingsRoute().push(context);
},
),
);
}
}
class _FingerprintingTile extends StatelessWidget {
const _FingerprintingTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Fingerprinting'),
subtitle: const Text('Language, fingerprint protection'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.fingerprint),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await FingerprintingSettingsRoute().push(context);
},
),
);
}
}
class _AdvancedTile extends StatelessWidget {
const _AdvancedTile();
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Advanced'),
subtitle: const Text('JavaScript, user agent, debugging'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(Icons.developer_mode),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await AdvancedSettingsRoute().push(context);
},
),
);
}
}
@@ -0,0 +1,359 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
class TabsBehaviorSettingsScreen extends StatelessWidget {
const TabsBehaviorSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tabs & Behavior')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_TabCreationSection(),
_TabOrganizationSection(),
_TabInteractionSection(),
],
);
},
),
),
);
}
}
class _TabCreationSection extends StatelessWidget {
const _TabCreationSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tab Creation'),
_NewTabDefaultSection(),
_ExternalLinkHandlingSection(),
],
);
}
}
class _TabOrganizationSection extends StatelessWidget {
const _TabOrganizationSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tab Organization'),
_CreateChildTabsTile(),
],
);
}
}
class _TabInteractionSection extends StatelessWidget {
const _TabInteractionSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Tab Interaction'),
_TabBarSwipeBehaviorSection(),
_ShowExtensionShortcutTile(),
],
);
}
}
class _NewTabDefaultSection extends HookConsumerWidget {
const _NewTabDefaultSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final defaultCreateTabType = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.defaultCreateTabType),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('New Tab Default'),
subtitle: Text(
'Choose the default type for manually created tabs',
),
leading: Icon(MdiIcons.tab),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: TabType.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
ButtonSegment(
value: TabType.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
],
selected: {defaultCreateTabType},
onSelectionChanged: (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.defaultCreateTabType(value.first),
);
},
style: switch (defaultCreateTabType) {
TabType.regular => null,
TabType.private => SegmentedButton.styleFrom(
selectedBackgroundColor: appColors.privateSelectionOverlay,
),
TabType.child => null,
},
),
),
],
),
);
}
}
class _ExternalLinkHandlingSection extends HookConsumerWidget {
const _ExternalLinkHandlingSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final tabIntentOpenSetting = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabIntentOpenSetting),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('External Link Handling'),
subtitle: Text(
'Choose how external links open in WebLibre',
),
leading: Icon(MdiIcons.tabPlus),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: TabIntentOpenSetting.ask,
label: Text('Prompt'),
icon: Icon(MdiIcons.messageQuestion),
),
ButtonSegment(
value: TabIntentOpenSetting.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
ButtonSegment(
value: TabIntentOpenSetting.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
],
selected: {tabIntentOpenSetting},
onSelectionChanged: (value) async {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.tabIntentOpenSetting(value.first),
);
},
style: switch (tabIntentOpenSetting) {
TabIntentOpenSetting.regular => null,
TabIntentOpenSetting.private => SegmentedButton.styleFrom(
selectedBackgroundColor: appColors.privateSelectionOverlay,
),
TabIntentOpenSetting.ask => null,
},
),
),
],
),
);
}
}
class _CreateChildTabsTile extends HookConsumerWidget {
const _CreateChildTabsTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final createChildTabsOption = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.createChildTabsOption),
);
return SwitchListTile.adaptive(
title: const Text('Create Child Tabs'),
subtitle: const Text(
'Display a button to create a child tab under the current tab (tree view only)',
),
secondary: const Icon(MdiIcons.fileTree),
value: createChildTabsOption,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.createChildTabsOption(value),
);
},
);
}
}
class _ShowExtensionShortcutTile extends HookConsumerWidget {
const _ShowExtensionShortcutTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final showExtensionShortcut = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showExtensionShortcut),
);
return SwitchListTile.adaptive(
title: const Text('Show Extension Shortcut'),
subtitle: const Text(
'Display an extension menu directly on the tab bar',
),
secondary: const Icon(MdiIcons.puzzleHeart),
value: showExtensionShortcut,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.showExtensionShortcut(value),
);
},
);
}
}
class _TabBarSwipeBehaviorSection extends HookConsumerWidget {
const _TabBarSwipeBehaviorSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabBarSwipeAction = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarSwipeAction),
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('Tab Bar Swipe Behavior'),
leading: Icon(MdiIcons.gestureSwipeHorizontal),
contentPadding: EdgeInsets.zero,
),
RadioGroup(
groupValue: tabBarSwipeAction,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveGeneralSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.tabBarSwipeAction(value),
);
}
},
child: const Column(
children: [
RadioListTile.adaptive(
value: TabBarSwipeAction.switchLastOpened,
title: Text('Switch to Last Used Tab'),
subtitle: Text(
'Swipe to toggle between current and previously opened tab',
),
),
RadioListTile.adaptive(
value: TabBarSwipeAction.navigateOrderedTabs,
title: Text('Navigate Sequential Tabs'),
subtitle: Text(
'Swipe left/right to move through tabs in order',
),
),
],
),
),
],
),
);
}
}
@@ -27,7 +27,7 @@ class SettingSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,