initial onboarding

This commit is contained in:
Fabian Freund
2025-06-14 23:33:24 +02:00
parent 11a1e8e5f5
commit bce9f0b3a5
26 changed files with 918 additions and 12 deletions
@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:weblibre/core/providers/router.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
import 'package:weblibre/features/onboarding/presentation/pages/default_search.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';
class OnboardingScreen extends HookConsumerWidget {
final int currentRevision;
final int targetRevision;
const OnboardingScreen({
super.key,
required this.currentRevision,
required this.targetRevision,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final pageController = usePageController();
final pages = useMemoized<List<Widget>>(() {
return [
const WelcomePage(),
const DefaultSearchPage(),
UBlockOptInPage(formKey: GlobalKey<FormState>()),
];
}, [currentRevision, targetRevision]);
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();
}
}
lastPage.value = value;
currentPage.value = value;
},
),
),
Row(
children: [
Expanded(
child: Visibility(
visible: currentPage.value > 0,
child: TextButton.icon(
onPressed: () async {
await pageController.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
},
icon: const Icon(Icons.chevron_left),
label: const Text('Previous'),
),
),
),
Expanded(
child: Center(
child: SmoothPageIndicator(
controller: pageController,
count: pages.length,
effect: WormEffect(
dotColor: theme.colorScheme.onSurface,
activeDotColor: theme.colorScheme.primary,
),
),
),
),
if (currentPage.value < pages.length - 1)
Expanded(
child: TextButton.icon(
onPressed: () async {
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);
ref.invalidate(routerProvider);
},
iconAlignment: IconAlignment.end,
icon: const Icon(Icons.done),
label: const Text('Done'),
),
),
],
),
],
),
),
);
}
}
@@ -0,0 +1,5 @@
import 'package:flutter/material.dart';
abstract class IFormPage {
GlobalKey<FormState> get formKey;
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
const defaultBangs = ['ddg', 'brave', 'startpage', 'qwant'];
class DefaultSearchPage extends HookConsumerWidget {
const DefaultSearchPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bangs = ref.watch(bangListProvider(triggers: defaultBangs));
final activeBang = ref.watch(defaultSearchBangDataProvider).valueOrNull;
Future<void> updateSearchProvider(String trigger) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.defaultSearchProvider(trigger),
);
}
return bangs.when(
skipLoadingOnReload: true,
data: (availableBangs) {
return ListView(
children: [
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Default Search:'),
if (activeBang != null)
FilterChip(
showCheckmark: false,
label: Text(activeBang.websiteName),
avatar: UrlIcon([
activeBang.getTemplateUrl(''),
], iconSize: 20),
selected: true,
onSelected: (value) {},
),
],
),
),
const Divider(),
Wrap(
spacing: 8.0,
children: [
...availableBangs.map(
(bang) => FilterChip(
showCheckmark: false,
label: Text(bang.websiteName),
avatar: UrlIcon([bang.getTemplateUrl('')], iconSize: 20),
selected: activeBang?.trigger == bang.trigger,
onSelected: (selected) async {
if (selected) {
await updateSearchProvider(bang.trigger);
}
},
),
),
ActionChip(
label: const Text('Search more'),
avatar: const Icon(Icons.search),
onPressed: () async {
final trigger = await const BangSearchRoute().push<String?>(
context,
);
if (trigger != null) {
await updateSearchProvider(trigger);
}
},
),
],
),
],
);
},
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(height: 48, width: double.infinity),
);
}
}
@@ -0,0 +1,66 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
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';
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
@override
final GlobalKey<FormState> formKey;
const UBlockOptInPage({super.key, required this.formKey});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Form(
key: formKey,
child: ListView(
children: [
SvgPicture.asset('assets/images/ublock.svg'),
const SizedBox(height: 8),
Center(
child: Text('uBlock Origin', style: theme.textTheme.headlineLarge),
),
const SizedBox(height: 24),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: 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**.
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(
value: field.value ?? false,
title: const Text('Install uBlock Origin Extension'),
onChanged: (value) {
field.didChange(value);
},
),
),
],
),
);
}
}
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class WelcomePage extends StatelessWidget {
const WelcomePage({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
FractionallySizedBox(
widthFactor: 0.5,
child: Image.asset('assets/icon/icon.png'),
),
Text(
'The Privacy-Focused Browser',
softWrap: true,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineLarge,
),
],
);
}
}