update onboarding; add missing safe areas
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
enum OnboardingMode {
|
||||
express,
|
||||
detailed,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class OnboardingModeNotifier extends _$OnboardingModeNotifier {
|
||||
@override
|
||||
OnboardingMode build() => OnboardingMode.detailed;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void select(OnboardingMode mode) {
|
||||
state = mode;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class EulaAcceptedNotifier extends _$EulaAcceptedNotifier {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void accept(bool value) {
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(OnboardingModeNotifier)
|
||||
final onboardingModeProvider = OnboardingModeNotifierProvider._();
|
||||
|
||||
final class OnboardingModeNotifierProvider
|
||||
extends $NotifierProvider<OnboardingModeNotifier, OnboardingMode> {
|
||||
OnboardingModeNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'onboardingModeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$onboardingModeNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
OnboardingModeNotifier create() => OnboardingModeNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(OnboardingMode value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<OnboardingMode>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$onboardingModeNotifierHash() =>
|
||||
r'bbb8e7ec1c85699566750d90c92c273e031d654b';
|
||||
|
||||
abstract class _$OnboardingModeNotifier extends $Notifier<OnboardingMode> {
|
||||
OnboardingMode build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<OnboardingMode, OnboardingMode>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<OnboardingMode, OnboardingMode>,
|
||||
OnboardingMode,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(EulaAcceptedNotifier)
|
||||
final eulaAcceptedProvider = EulaAcceptedNotifierProvider._();
|
||||
|
||||
final class EulaAcceptedNotifierProvider
|
||||
extends $NotifierProvider<EulaAcceptedNotifier, bool> {
|
||||
EulaAcceptedNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'eulaAcceptedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$eulaAcceptedNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
EulaAcceptedNotifier create() => EulaAcceptedNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$eulaAcceptedNotifierHash() =>
|
||||
r'dafd708ac5cb586e8ca9cccc6d708598c5daab0e';
|
||||
|
||||
abstract class _$EulaAcceptedNotifier extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
* 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 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -25,11 +27,17 @@ import 'package:weblibre/core/providers/defaults.dart';
|
||||
import 'package:weblibre/core/providers/router.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||
import 'package:weblibre/features/onboarding/domain/providers.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/ai_configuration.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/default_search.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/doh_settings.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/permissions.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/privacy_hardening.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/toolbar_layout.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/welcome.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||
@@ -50,6 +58,12 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
|
||||
final pageController = usePageController();
|
||||
final eulaAccepted = ref.watch(eulaAcceptedProvider);
|
||||
final onboardingMode = ref.watch(onboardingModeProvider);
|
||||
|
||||
final isReturningUser = currentRevision == 2;
|
||||
final showDetailedPages =
|
||||
isReturningUser || onboardingMode == OnboardingMode.detailed;
|
||||
|
||||
final pages = useMemoized<List<Widget>>(() {
|
||||
switch (currentRevision) {
|
||||
@@ -57,122 +71,156 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
return [const AiConfigurationPage()];
|
||||
default:
|
||||
return [
|
||||
const WelcomePage(),
|
||||
WelcomePage(isReturningUser: isReturningUser),
|
||||
const DefaultSearchPage(),
|
||||
if (showDetailedPages) const DohSettingsPage(),
|
||||
if (showDetailedPages) const ToolbarLayoutPage(),
|
||||
const PrivacyHardeningPage(),
|
||||
const AiConfigurationPage(),
|
||||
UBlockOptInPage(formKey: GlobalKey<FormState>()),
|
||||
if (showDetailedPages)
|
||||
UBlockOptInPage(formKey: GlobalKey<FormState>()),
|
||||
PermissionsPage(formKey: GlobalKey<FormState>()),
|
||||
];
|
||||
}
|
||||
}, [currentRevision, targetRevision]);
|
||||
}, [currentRevision, targetRevision, onboardingMode]);
|
||||
|
||||
final lastPage = useRef(pageController.initialPage);
|
||||
final currentPage = useState(pageController.initialPage);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
children: pages,
|
||||
onPageChanged: (value) {
|
||||
if (value > lastPage.value) {
|
||||
if (pages[lastPage.value] case final IFormPage formPage) {
|
||||
formPage.formKey.currentState?.save();
|
||||
return PopScope(
|
||||
canPop: currentPage.value == 0,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(currentPage.value - 1);
|
||||
} else {
|
||||
await pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
physics: currentPage.value == 0 && !eulaAccepted
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: pages,
|
||||
onPageChanged: (value) {
|
||||
if (value > lastPage.value) {
|
||||
if (pages[lastPage.value] case final IFormPage formPage) {
|
||||
formPage.formKey.currentState?.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastPage.value = value;
|
||||
currentPage.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Visibility(
|
||||
visible: currentPage.value > 0,
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(currentPage.value - 1);
|
||||
} else {
|
||||
await pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
label: const Text('Previous'),
|
||||
),
|
||||
),
|
||||
lastPage.value = value;
|
||||
currentPage.value = value;
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SmoothPageIndicator(
|
||||
controller: pageController,
|
||||
count: pages.length,
|
||||
effect: WormEffect(
|
||||
dotColor: theme.colorScheme.onSurface,
|
||||
activeDotColor: theme.colorScheme.primary,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Visibility(
|
||||
visible: currentPage.value > 0,
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(currentPage.value - 1);
|
||||
} else {
|
||||
await pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
label: const Text('Previous'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentPage.value < pages.length - 1)
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(currentPage.value + 1);
|
||||
} else {
|
||||
await pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
iconAlignment: IconAlignment.end,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
label: const Text('Next'),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
if (pages[currentPage.value]
|
||||
case final IFormPage formPage) {
|
||||
formPage.formKey.currentState?.save();
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(onboardingRepositoryProvider.notifier)
|
||||
.pushRevision(targetRevision);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: ref.read(docsUriProvider),
|
||||
tabMode: TabMode.regular,
|
||||
containerSelection:
|
||||
const TabContainerSelection.unassigned(),
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
ref.invalidate(routerProvider);
|
||||
},
|
||||
iconAlignment: IconAlignment.end,
|
||||
icon: const Icon(Icons.done),
|
||||
label: const Text('Done'),
|
||||
child: Center(
|
||||
child: SmoothPageIndicator(
|
||||
controller: pageController,
|
||||
count: pages.length,
|
||||
effect: WormEffect(
|
||||
activeDotColor: theme.colorScheme.primary,
|
||||
dotHeight: 10.0,
|
||||
dotWidth: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (currentPage.value < pages.length - 1)
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: currentPage.value == 0 && !eulaAccepted
|
||||
? null
|
||||
: () async {
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(
|
||||
currentPage.value + 1,
|
||||
);
|
||||
} else {
|
||||
await pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
iconAlignment: IconAlignment.end,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
label: const Text('Next'),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
if (pages[currentPage.value]
|
||||
case final IFormPage formPage) {
|
||||
formPage.formKey.currentState?.save();
|
||||
}
|
||||
|
||||
if (onboardingMode == OnboardingMode.express) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(browserAddonServiceProvider.notifier)
|
||||
.install('uBlock0@raymondhill.net'),
|
||||
);
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(onboardingRepositoryProvider.notifier)
|
||||
.pushRevision(targetRevision);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: ref.read(docsUriProvider),
|
||||
tabMode: TabMode.regular,
|
||||
containerSelection:
|
||||
const TabContainerSelection.unassigned(),
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
ref.invalidate(routerProvider);
|
||||
},
|
||||
iconAlignment: IconAlignment.end,
|
||||
icon: const Icon(Icons.done),
|
||||
label: const Text('Done'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_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/presentation/widgets/browser_page.dart';
|
||||
|
||||
class AiConfigurationPage extends HookConsumerWidget {
|
||||
const AiConfigurationPage({super.key});
|
||||
@@ -37,71 +38,77 @@ class AiConfigurationPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text('AI Features', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
height: 1.4,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Things to keep in mind\n\n',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'WebLibre uses a local AI model to analyze your open tab titles and suggest container tabs and names. All processing happens entirely on your device.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'AI enhancements operate entirely within your browser, keeping all data on your device. Local AI processing respects your privacy and provides faster suggestions for container groups and names. You can control this behavior anytime through settings.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'AI can sometimes make mistakes, so please review suggested group names and tab selections.',
|
||||
),
|
||||
],
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text('AI Features', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
height: 1.4,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Things to keep in mind\n\n',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'WebLibre uses a local AI model to analyze your open tab titles and suggest container tabs and names. All processing happens entirely on your device.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'AI enhancements operate entirely within your browser, keeping all data on your device. Local AI processing respects your privacy and provides faster suggestions for container groups and names. You can control this behavior anytime through settings.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'AI can sometimes make mistakes, so please review suggested group names and tab selections.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import 'package:weblibre/features/settings/presentation/controllers/save_setting
|
||||
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
|
||||
@@ -61,12 +62,13 @@ class DefaultSearchPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: bangs.when(
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: bangs.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (availableBangs) {
|
||||
return ListView(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
@@ -172,6 +174,7 @@ class DefaultSearchPage extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const SizedBox(height: 48, width: double.infinity),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/doh_settings_content.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class DohSettingsPage extends HookConsumerWidget {
|
||||
const DohSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text(
|
||||
'DNS over HTTPS',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const ListTileTheme(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
child: DohSettingsContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,14 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
||||
show GeckoBrowserService;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
||||
@override
|
||||
@@ -38,44 +41,79 @@ class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
||||
() => Permission.notification.isGranted,
|
||||
);
|
||||
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text('Permissions', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// child: const Text('WebLibre requires following permissions'),
|
||||
// ),
|
||||
// const SizedBox(height: 24),
|
||||
Skeletonizer(
|
||||
enabled: !notificationPermissionEnabled.hasData,
|
||||
child: FormField(
|
||||
initialValue: true,
|
||||
onSaved: (newValue) async {
|
||||
if (newValue == true) {
|
||||
await Permission.notification.request();
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
value: field.value ?? true,
|
||||
title: const Text('Notifications'),
|
||||
subtitle: const Text(
|
||||
'Required to inform about download status',
|
||||
final isDefaultBrowser = useCachedFuture(
|
||||
() => GeckoBrowserService().isDefaultBrowser(),
|
||||
);
|
||||
|
||||
final isCurrentDefaultBrowser = isDefaultBrowser.data == true;
|
||||
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text(
|
||||
'Permissions',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
onChanged: (notificationPermissionEnabled.data == true)
|
||||
? null
|
||||
: (value) {
|
||||
field.didChange(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Skeletonizer(
|
||||
enabled: !notificationPermissionEnabled.hasData,
|
||||
child: FormField(
|
||||
initialValue: true,
|
||||
onSaved: (newValue) async {
|
||||
if (newValue == true &&
|
||||
notificationPermissionEnabled.data != true) {
|
||||
await Permission.notification.request();
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: field.value ?? true,
|
||||
title: const Text('Notifications'),
|
||||
subtitle: const Text(
|
||||
'Required to inform about download status',
|
||||
),
|
||||
onChanged: (notificationPermissionEnabled.data == true)
|
||||
? null
|
||||
: (value) {
|
||||
field.didChange(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled: !isDefaultBrowser.hasData,
|
||||
child: FormField(
|
||||
key: ValueKey(isDefaultBrowser.data),
|
||||
initialValue: isCurrentDefaultBrowser,
|
||||
onSaved: (newValue) async {
|
||||
if (newValue == true && isDefaultBrowser.data == false) {
|
||||
await GeckoBrowserService().requestDefaultBrowser();
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: field.value ?? isCurrentDefaultBrowser,
|
||||
title: const Text('Default Browser'),
|
||||
subtitle: const Text(
|
||||
'Set WebLibre as your default browser',
|
||||
),
|
||||
onChanged: isCurrentDefaultBrowser
|
||||
? null
|
||||
: (value) {
|
||||
field.didChange(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* 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: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/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/setting_groups_serializer.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class PrivacyHardeningPage extends ConsumerWidget {
|
||||
const PrivacyHardeningPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text(
|
||||
'Privacy & Hardening',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const _LanguageFingerprintWarning(),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Browser Languages'),
|
||||
subtitle: const Text(
|
||||
'Configure language preferences exposed to websites',
|
||||
),
|
||||
leading: const Icon(Icons.translate),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await LocaleSettingsRoute().push(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _WebEngineHardeningToggle(),
|
||||
const SizedBox(height: 16),
|
||||
const _FingerprintProtectionToggle(),
|
||||
const SizedBox(height: 16),
|
||||
const _LocalNetworkAccessSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageFingerprintWarning extends ConsumerWidget {
|
||||
const _LanguageFingerprintWarning();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final configuredLocales = ref.watch(
|
||||
engineSettingsWithDefaultsProvider.select((s) => s.locales),
|
||||
);
|
||||
|
||||
if (configuredLocales.length <= 1) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
height: 1.4,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Multiple Languages Detected\n\n',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
TextSpan(
|
||||
text:
|
||||
'Your browser has ${configuredLocales.length} languages '
|
||||
'configured (${configuredLocales.join(', ')}). '
|
||||
'Websites can use your unique language combination to '
|
||||
'fingerprint and track you across the web.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'Consider reducing your browser languages to a single '
|
||||
'language to minimize your fingerprint surface.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.onErrorContainer,
|
||||
foregroundColor: theme.colorScheme.errorContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
await LocaleSettingsRoute().push(context);
|
||||
},
|
||||
icon: const Icon(Icons.translate),
|
||||
label: const Text('Review Languages'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WebEngineHardeningToggle extends ConsumerWidget {
|
||||
const _WebEngineHardeningToggle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final preferenceGroups = ref.watch(
|
||||
unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user),
|
||||
);
|
||||
|
||||
final allGroupsActive = preferenceGroups.maybeWhen(
|
||||
data: (data) =>
|
||||
data.values.every((element) => element.isActiveOrOptional),
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Complete Web Engine Hardening'),
|
||||
subtitle: const Text(
|
||||
'Apply all recommended security hardening preferences to the web engine',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.shieldLock),
|
||||
value: allGroupsActive,
|
||||
onChanged: preferenceGroups.hasValue
|
||||
? (value) async {
|
||||
final notifier = ref.read(
|
||||
unifiedPreferenceSettingsRepositoryProvider(
|
||||
PreferencePartition.user,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
if (value) {
|
||||
await notifier.apply();
|
||||
} else {
|
||||
await notifier.reset();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FingerprintProtectionToggle extends ConsumerWidget {
|
||||
const _FingerprintProtectionToggle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final currentOverrides = ref.watch(
|
||||
engineSettingsWithDefaultsProvider.select(
|
||||
(s) => s.fingerprintingProtectionOverrides,
|
||||
),
|
||||
);
|
||||
|
||||
final isHardened =
|
||||
currentOverrides == FingerprintOverrides.hardenedDefaults().toString();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Hardened Fingerprint Protection'),
|
||||
subtitle: const Text(
|
||||
'Load comprehensive fingerprint protection defaults',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.fingerprint),
|
||||
value: isHardened,
|
||||
onChanged: (value) async {
|
||||
final overrides = value
|
||||
? FingerprintOverrides.hardenedDefaults()
|
||||
: FingerprintOverrides.defaults();
|
||||
|
||||
await ref
|
||||
.read(saveEngineSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.fingerprintingProtectionOverrides(overrides.toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
alignment: Alignment.topCenter,
|
||||
child: isHardened
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onError,
|
||||
height: 1.4,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Compatibility Warning\n\n',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.onError,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'Hardened fingerprint protection enables 60+ '
|
||||
'protection targets including canvas '
|
||||
'randomization, navigator spoofing, media device '
|
||||
'masking, and more.\n\n',
|
||||
),
|
||||
const TextSpan(text: '• '),
|
||||
const TextSpan(
|
||||
text:
|
||||
'This may cause websites to break or behave '
|
||||
'unexpectedly. You can fine-tune individual '
|
||||
'targets in settings.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalNetworkAccessSection extends ConsumerWidget {
|
||||
const _LocalNetworkAccessSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final lnaBlocking = ref.watch(
|
||||
engineSettingsWithDefaultsProvider.select((s) => s.lnaBlocking),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('Local Network Protection'),
|
||||
subtitle: Text(
|
||||
'Websites can try to reach your device and other devices '
|
||||
'on your home network, like routers, printers, or smart home '
|
||||
'devices. By default, known trackers are automatically blocked '
|
||||
'from doing this.',
|
||||
),
|
||||
leading: Icon(MdiIcons.lanDisconnect),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Block All Local Network Requests'),
|
||||
subtitle: const Text(
|
||||
'Ask for permission before any website accesses devices '
|
||||
'on your home network, not just known trackers',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.shieldLockOpen),
|
||||
value: lnaBlocking ?? false,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveEngineSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.lnaBlocking(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/toolbar_layout_content.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/toolbar_preview.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class ToolbarLayoutPage extends HookConsumerWidget {
|
||||
const ToolbarLayoutPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
|
||||
return BrowserPage(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 32, 12, 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Toolbar & Layout',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: TabBarPreviewHeaderDelegate(
|
||||
settings: settings,
|
||||
backgroundColor: Colors.transparent,
|
||||
compact: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 32),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: const ListTileTheme(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
child: ToolbarLayoutContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||
@override
|
||||
@@ -36,50 +37,52 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
SvgPicture.asset('assets/images/ublock.svg'),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text('uBlock Origin', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: MarkdownBody(
|
||||
data: '''
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
SvgPicture.asset('assets/images/ublock.svg'),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text('uBlock Origin', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const MarkdownBody(
|
||||
data: '''
|
||||
uBlock Origin (uBO) is a CPU and memory-efficient **wide-spectrum content blocker** by **Raymond Hill** and available as a browser extensions for WebLibre.
|
||||
|
||||
It blocks ads, trackers, coin miners, popups, annoying anti-blockers, malware sites, etc., by default using **EasyList, EasyPrivacy, Peter Lowe's Blocklist, Online Malicious URL Blocklist, and uBO filter lists**.
|
||||
It blocks ads, trackers, coin miners, popups, annoying anti-blockers, malware sites, etc., by default using **EasyList, EasyPrivacy, Peter Lowe's Blocklist, Online Malicious URL Blocklist, and uBO filter lists**.
|
||||
|
||||
There are many other lists available to block even more.
|
||||
''',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormField(
|
||||
initialValue: true,
|
||||
onSaved: (newValue) {
|
||||
if (newValue == true) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(browserAddonServiceProvider.notifier)
|
||||
.install('uBlock0@raymondhill.net'),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: field.value ?? false,
|
||||
title: const Text('Install uBlock Origin Extension'),
|
||||
onChanged: (value) {
|
||||
field.didChange(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormField(
|
||||
initialValue: true,
|
||||
onSaved: (newValue) {
|
||||
if (newValue == true) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(browserAddonServiceProvider.notifier)
|
||||
.install('uBlock0@raymondhill.net'),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
value: field.value ?? false,
|
||||
title: const Text('Install uBlock Origin Extension'),
|
||||
onChanged: (value) {
|
||||
field.didChange(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,28 +18,390 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||
import 'package:weblibre/features/onboarding/domain/providers.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class WelcomePage extends StatelessWidget {
|
||||
const WelcomePage({super.key});
|
||||
class WelcomePage extends ConsumerWidget {
|
||||
final bool isReturningUser;
|
||||
|
||||
const WelcomePage({super.key, this.isReturningUser = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
final selectedMode = ref.watch(onboardingModeProvider);
|
||||
final accepted = ref.watch(eulaAcceptedProvider);
|
||||
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BrandHeader(colorScheme: colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
isReturningUser ? 'Welcome back!' : 'WebLibre is ready',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (isReturningUser) ...[
|
||||
const SizedBox(height: 16),
|
||||
_UpdateNotice(colorScheme: colorScheme, theme: theme),
|
||||
],
|
||||
if (!isReturningUser) ...[
|
||||
const SizedBox(height: 40),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'Choose your onboarding experience:',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_ModeOption(
|
||||
mode: OnboardingMode.express,
|
||||
title: 'Quick Start',
|
||||
subtitle: 'Use recommended defaults and get browsing.',
|
||||
icon: Icons.bolt,
|
||||
selected: selectedMode == OnboardingMode.express,
|
||||
onTap: () => ref
|
||||
.read(onboardingModeProvider.notifier)
|
||||
.select(OnboardingMode.express),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_ModeOption(
|
||||
mode: OnboardingMode.detailed,
|
||||
title: 'Custom Setup',
|
||||
subtitle: 'Configure DNS, toolbar, extensions, and more.',
|
||||
icon: Icons.tune,
|
||||
selected: selectedMode == OnboardingMode.detailed,
|
||||
onTap: () => ref
|
||||
.read(onboardingModeProvider.notifier)
|
||||
.select(OnboardingMode.detailed),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
_EulaCheckbox(
|
||||
accepted: accepted,
|
||||
onAcceptedChanged: (value) =>
|
||||
ref.read(eulaAcceptedProvider.notifier).accept(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateNotice extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final ThemeData theme;
|
||||
|
||||
const _UpdateNotice({required this.colorScheme, required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: SvgPicture.asset('assets/icon/icon.svg'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'The Privacy-Focused Browser',
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.auto_awesome,
|
||||
color: BrowserPage.brandYellow,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'A lot has changed!',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'This update includes significant changes that require '
|
||||
'you to review your settings. Please walk through the '
|
||||
'following pages to revalidate your configuration.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.extension,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Please re-check your extensions after this update '
|
||||
'due to known migration issues.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your existing settings will not be overridden '
|
||||
'unless you explicitly change them during this setup.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeOption extends StatelessWidget {
|
||||
final OnboardingMode mode;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ModeOption({
|
||||
required this.mode,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? colorScheme.primaryContainer.withValues(alpha: 0.4)
|
||||
: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: selected ? colorScheme.primary : Colors.transparent,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: selected
|
||||
? BrowserPage.brandYellow
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: selected
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selected
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (selected) Icon(Icons.check_circle, color: colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EulaCheckbox extends StatelessWidget {
|
||||
final bool accepted;
|
||||
final ValueChanged<bool> onAcceptedChanged;
|
||||
|
||||
const _EulaCheckbox({
|
||||
required this.accepted,
|
||||
required this.onAcceptedChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return CheckboxListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: accepted,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (value) => onAcceptedChanged(value ?? false),
|
||||
title: Wrap(
|
||||
children: [
|
||||
Text(
|
||||
'I have read and accept the ',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => _showLegalDocument(
|
||||
context,
|
||||
title: 'End User License Agreement',
|
||||
assetPath: 'assets/legal/EULA.md',
|
||||
),
|
||||
child: Text(
|
||||
'EULA',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: BrowserPage.brandYellow,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: BrowserPage.brandYellow,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' and ',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => _showLegalDocument(
|
||||
context,
|
||||
title: 'Privacy Policy',
|
||||
assetPath: 'assets/legal/PRIVACY_POLICY.md',
|
||||
),
|
||||
child: Text(
|
||||
'Privacy Policy',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: BrowserPage.brandYellow,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: BrowserPage.brandYellow,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLegalDocument(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String assetPath,
|
||||
}) async {
|
||||
final content = await rootBundle.loadString(assetPath);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
expand: false,
|
||||
builder: (context, scrollController) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: Markdown(controller: scrollController, data: content),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user