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
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
viewBox="0 0 128 128"
height="128"
width="128">
<g
style="display:inline;opacity:1">
<g
style="fill:#800000;fill-opacity:1;stroke:#ffffff;stroke-width:1.62100744;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
transform="matrix(0.6778654,0,0,0.56141828,-241.07537,-247.27712)" />
<g
transform="matrix(-0.6945203,0,0,0.56109687,375.02964,-247.42947)"
style="fill:#800000;fill-opacity:1;stroke:#ffffff;stroke-width:1.60191178000000001;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;stroke-linejoin:round">
<path
d="m 447.83376,669.09921 c -80.63119,-57.03115 -80.63119,-57.03115 -80.63119,-199.60903 34.55623,0 46.07497,0 80.63119,-28.51558 m 0,228.12461 c 80.6312,-57.03115 80.6312,-57.03115 80.6312,-199.60903 -34.55623,0 -46.07497,0 -80.6312,-28.51558"
style="fill:#800000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1.60191178;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
</g>
<g
transform="translate(-17.872548,-17.32535)"
style="display:inline">
<ellipse
transform="matrix(1.3333335,0,0,1.3333334,-42.290862,-31.108461)"
ry="12"
rx="11.999999"
cy="81.325356"
cx="102.12254"
style="fill:none;stroke:#ffffff;stroke-width:5.99999952;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;" />
<g
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.99999996999999996;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
transform="scale(1.0018026,0.99820067)">
<path
d="m 81.72523,81.471945 c 0,11.019828 -4.991003,16.028841 -15.97121,16.028841 -10.980207,0 -15.97121,-5.009013 -15.97121,-16.028841 l 0,-24.043262 7.985605,0 0,24.043262 c 0,7.012618 0.9982,8.014421 7.985605,8.014421 6.987404,0 7.985605,-1.001803 7.985605,-8.014421 l 0,-24.043262 7.985605,0 z"
style="fill:#ffffff;stroke:#ffffff;stroke-width:0;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+26 -2
View File
@@ -2,10 +2,34 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
part 'router.g.dart';
@Riverpod(keepAlive: true)
GoRouter router(Ref ref) {
return GoRouter(debugLogDiagnostics: true, routes: $appRoutes);
Future<GoRouter> router(Ref ref) async {
String? initialLocation;
final onboardingMandatory = await ref
.read(onboardingRepositoryProvider.notifier)
.isOutdated();
if (onboardingMandatory) {
final current = await ref
.read(onboardingRepositoryProvider.notifier)
.getCurrentRevision();
final route = OnboardingRoute(
currentRevision: current ?? -1,
targetRevision: OnboardingRepository.targetRevision,
);
initialLocation = route.location;
}
return GoRouter(
debugLogDiagnostics: true,
routes: $appRoutes,
initialLocation: initialLocation,
);
}
+3 -3
View File
@@ -6,11 +6,11 @@ part of 'router.dart';
// RiverpodGenerator
// **************************************************************************
String _$routerHash() => r'25fc899b87c080600f6e1d87d510db333378bdfd';
String _$routerHash() => r'ce7cc5b5f236781a06867a852a08ed03d3582022';
/// See also [router].
@ProviderFor(router)
final routerProvider = Provider<GoRouter>.internal(
final routerProvider = FutureProvider<GoRouter>.internal(
router,
name: r'routerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,6 +22,6 @@ final routerProvider = Provider<GoRouter>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef RouterRef = ProviderRef<GoRouter>;
typedef RouterRef = FutureProviderRef<GoRouter>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
+23
View File
@@ -16,6 +16,7 @@ import 'package:weblibre/features/geckoview/features/search/presentation/screens
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart';
import 'package:weblibre/features/onboarding/presentation/onboarding.dart';
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/general_settings.dart';
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
@@ -43,3 +44,25 @@ class AboutRoute extends GoRouteData with _$AboutRoute {
return DialogPage(builder: (_) => const AboutDialogScreen());
}
}
@TypedGoRoute<OnboardingRoute>(
name: 'OnboardingRoute',
path: '/onboarding/:currentRevision/:targetRevision',
)
class OnboardingRoute extends GoRouteData with _$OnboardingRoute {
final int currentRevision;
final int targetRevision;
const OnboardingRoute({
required this.currentRevision,
required this.targetRevision,
});
@override
Widget build(BuildContext context, GoRouterState state) {
return OnboardingScreen(
currentRevision: currentRevision,
targetRevision: targetRevision,
);
}
}
+35
View File
@@ -8,6 +8,7 @@ part of 'routes.dart';
List<RouteBase> get $appRoutes => [
$aboutRoute,
$onboardingRoute,
$settingsRoute,
$browserRoute,
$bangCategoriesRoute,
@@ -41,6 +42,40 @@ mixin _$AboutRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $onboardingRoute => GoRouteData.$route(
path: '/onboarding/:currentRevision/:targetRevision',
name: 'OnboardingRoute',
factory: _$OnboardingRoute._fromState,
);
mixin _$OnboardingRoute on GoRouteData {
static OnboardingRoute _fromState(GoRouterState state) => OnboardingRoute(
currentRevision: int.parse(state.pathParameters['currentRevision']!)!,
targetRevision: int.parse(state.pathParameters['targetRevision']!)!,
);
OnboardingRoute get _self => this as OnboardingRoute;
@override
String get location => GoRouteData.$location(
'/onboarding/${Uri.encodeComponent(_self.currentRevision.toString())}/${Uri.encodeComponent(_self.targetRevision.toString())}',
);
@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);
}
RouteBase get $settingsRoute => GoRouteData.$route(
path: '/settings',
name: 'SettingsRoute',
@@ -34,6 +34,7 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
}
Selectable<BangData> getBangDataList({
Iterable<String>? triggers,
Iterable<BangGroup>? groups,
String? domain,
String? category,
@@ -41,6 +42,9 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
bool? orderMostFrequentFirst,
}) {
final selectable = select(db.bangDataView);
if (triggers != null) {
selectable.where((t) => t.trigger.isIn(triggers));
}
if (groups != null) {
selectable.where((t) => t.group.isInValues(groups));
}
@@ -30,6 +30,7 @@ Stream<Map<String, List<String>>> bangCategories(Ref ref) {
@Riverpod()
Stream<List<BangData>> bangList(
Ref ref, {
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
@@ -37,6 +38,7 @@ Stream<List<BangData>> bangList(
}) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBangs(
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
@@ -43,7 +43,7 @@ final bangCategoriesProvider =
// ignore: unused_element
typedef BangCategoriesRef =
AutoDisposeStreamProviderRef<Map<String, List<String>>>;
String _$bangListHash() => r'dc168bd1246bea3e5f8015dd52fbf41c5f98ca13';
String _$bangListHash() => r'd1e0bb9fa4f523ce516e075c0d149bf7803ebb2b';
/// Copied from Dart SDK
class _SystemHash {
@@ -77,12 +77,14 @@ class BangListFamily extends Family<AsyncValue<List<BangData>>> {
/// See also [bangList].
BangListProvider call({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
}) {
return BangListProvider(
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
@@ -93,6 +95,7 @@ class BangListFamily extends Family<AsyncValue<List<BangData>>> {
@override
BangListProvider getProviderOverride(covariant BangListProvider provider) {
return call(
triggers: provider.triggers,
groups: provider.groups,
domain: provider.domain,
categoryFilter: provider.categoryFilter,
@@ -119,6 +122,7 @@ class BangListFamily extends Family<AsyncValue<List<BangData>>> {
class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
/// See also [bangList].
BangListProvider({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
@@ -126,6 +130,7 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
}) : this._internal(
(ref) => bangList(
ref as BangListRef,
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
@@ -138,6 +143,7 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
: _$bangListHash,
dependencies: BangListFamily._dependencies,
allTransitiveDependencies: BangListFamily._allTransitiveDependencies,
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
@@ -151,12 +157,14 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.triggers,
required this.groups,
required this.domain,
required this.categoryFilter,
required this.orderMostFrequentFirst,
}) : super.internal();
final List<String>? triggers;
final List<BangGroup>? groups;
final String? domain;
final ({String category, String? subCategory})? categoryFilter;
@@ -175,6 +183,7 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
@@ -191,6 +200,7 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
@override
bool operator ==(Object other) {
return other is BangListProvider &&
other.triggers == triggers &&
other.groups == groups &&
other.domain == domain &&
other.categoryFilter == categoryFilter &&
@@ -200,6 +210,7 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, triggers.hashCode);
hash = _SystemHash.combine(hash, groups.hashCode);
hash = _SystemHash.combine(hash, domain.hashCode);
hash = _SystemHash.combine(hash, categoryFilter.hashCode);
@@ -212,6 +223,9 @@ class BangListProvider extends AutoDisposeStreamProvider<List<BangData>> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BangListRef on AutoDisposeStreamProviderRef<List<BangData>> {
/// The parameter `triggers` of this provider.
List<String>? get triggers;
/// The parameter `groups` of this provider.
List<BangGroup>? get groups;
@@ -230,6 +244,8 @@ class _BangListProviderElement
with BangListRef {
_BangListProviderElement(super.provider);
@override
List<String>? get triggers => (origin as BangListProvider).triggers;
@override
List<BangGroup>? get groups => (origin as BangListProvider).groups;
@override
@@ -46,6 +46,7 @@ class BangDataRepository extends _$BangDataRepository {
}
Stream<List<BangData>> watchBangs({
Iterable<String>? triggers,
Iterable<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
@@ -55,6 +56,7 @@ class BangDataRepository extends _$BangDataRepository {
.read(bangDatabaseProvider)
.bangDao
.getBangDataList(
triggers: triggers,
groups: groups,
domain: domain,
category: categoryFilter?.category,
@@ -7,7 +7,7 @@ part of 'data.dart';
// **************************************************************************
String _$bangDataRepositoryHash() =>
r'de89d7089ce6a92015bee233f08d920981518793';
r'095708cfffe8d0fb604dc02f69e77ae1c050a893';
/// See also [BangDataRepository].
@ProviderFor(BangDataRepository)
@@ -22,7 +22,7 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
unawaited(
service.setActions([
SearchAction((text) async {
final router = ref.read(routerProvider);
final router = await ref.read(routerProvider.future);
final isCurrentPrivate =
ref.read(selectedTabStateProvider)?.isPrivate ?? false;
final route = SearchRoute(
@@ -7,7 +7,7 @@ part of 'providers.dart';
// **************************************************************************
String _$selectionActionServiceHash() =>
r'c45737a0a403bff1bdb1931b89701300f8dc3726';
r'456bab83920ae78969bece514d5524e2445e06b6';
/// See also [selectionActionService].
@ProviderFor(selectionActionService)
@@ -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,
),
],
);
}
}
@@ -0,0 +1,28 @@
import 'package:drift/drift.dart';
import 'package:weblibre/features/user/data/database/database.dart';
part 'onboarding.g.dart';
@DriftAccessor()
class OnboardingDao extends DatabaseAccessor<UserDatabase>
with _$OnboardingDaoMixin {
OnboardingDao(super.attachedDatabase);
SingleOrNullSelectable<int?> getLastRevision() {
final maxRevision = db.onboarding.revision.max();
final query = selectOnly(db.onboarding)..addColumns([maxRevision]);
return query.map((row) => row.read(maxRevision));
}
Future<void> pushRevision(int revision, DateTime completionDate) {
return db.onboarding.insertOne(
OnboardingCompanion.insert(
revision: revision,
completionDate: completionDate,
),
mode: InsertMode.insertOrReplace,
);
}
}
@@ -0,0 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'onboarding.dart';
// ignore_for_file: type=lint
mixin _$OnboardingDaoMixin on DatabaseAccessor<UserDatabase> {}
@@ -1,10 +1,14 @@
import 'package:drift/drift.dart';
import 'package:weblibre/features/user/data/database/daos/cache.dart';
import 'package:weblibre/features/user/data/database/daos/onboarding.dart';
import 'package:weblibre/features/user/data/database/daos/setting.dart';
part 'database.g.dart';
@DriftDatabase(include: {'database.drift'}, daos: [SettingDao, CacheDao])
@DriftDatabase(
include: {'database.drift'},
daos: [SettingDao, CacheDao, OnboardingDao],
)
class UserDatabase extends _$UserDatabase {
@override
final int schemaVersion = 1;
@@ -10,6 +10,11 @@ CREATE TABLE icon_cache (
fetch_date DATETIME NOT NULL
);
CREATE TABLE onboarding (
revision INTEGER NOT NULL,
completion_date DATETIME NOT NULL
);
evictCacheEntries:
DELETE FROM icon_cache
WHERE rowid IN (
@@ -446,13 +446,199 @@ class IconCacheCompanion extends UpdateCompanion<IconCacheData> {
}
}
class Onboarding extends Table with TableInfo<Onboarding, OnboardingData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
Onboarding(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<int> revision = GeneratedColumn<int>(
'revision',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
late final GeneratedColumn<DateTime> completionDate =
GeneratedColumn<DateTime>(
'completion_date',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
@override
List<GeneratedColumn> get $columns => [revision, completionDate];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'onboarding';
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
OnboardingData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return OnboardingData(
revision: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}revision'],
)!,
completionDate: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}completion_date'],
)!,
);
}
@override
Onboarding createAlias(String alias) {
return Onboarding(attachedDatabase, alias);
}
@override
bool get dontWriteConstraints => true;
}
class OnboardingData extends DataClass implements Insertable<OnboardingData> {
final int revision;
final DateTime completionDate;
const OnboardingData({required this.revision, required this.completionDate});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['revision'] = Variable<int>(revision);
map['completion_date'] = Variable<DateTime>(completionDate);
return map;
}
factory OnboardingData.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return OnboardingData(
revision: serializer.fromJson<int>(json['revision']),
completionDate: serializer.fromJson<DateTime>(json['completion_date']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'revision': serializer.toJson<int>(revision),
'completion_date': serializer.toJson<DateTime>(completionDate),
};
}
OnboardingData copyWith({int? revision, DateTime? completionDate}) =>
OnboardingData(
revision: revision ?? this.revision,
completionDate: completionDate ?? this.completionDate,
);
OnboardingData copyWithCompanion(OnboardingCompanion data) {
return OnboardingData(
revision: data.revision.present ? data.revision.value : this.revision,
completionDate: data.completionDate.present
? data.completionDate.value
: this.completionDate,
);
}
@override
String toString() {
return (StringBuffer('OnboardingData(')
..write('revision: $revision, ')
..write('completionDate: $completionDate')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(revision, completionDate);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is OnboardingData &&
other.revision == this.revision &&
other.completionDate == this.completionDate);
}
class OnboardingCompanion extends UpdateCompanion<OnboardingData> {
final Value<int> revision;
final Value<DateTime> completionDate;
final Value<int> rowid;
const OnboardingCompanion({
this.revision = const Value.absent(),
this.completionDate = const Value.absent(),
this.rowid = const Value.absent(),
});
OnboardingCompanion.insert({
required int revision,
required DateTime completionDate,
this.rowid = const Value.absent(),
}) : revision = Value(revision),
completionDate = Value(completionDate);
static Insertable<OnboardingData> custom({
Expression<int>? revision,
Expression<DateTime>? completionDate,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (revision != null) 'revision': revision,
if (completionDate != null) 'completion_date': completionDate,
if (rowid != null) 'rowid': rowid,
});
}
OnboardingCompanion copyWith({
Value<int>? revision,
Value<DateTime>? completionDate,
Value<int>? rowid,
}) {
return OnboardingCompanion(
revision: revision ?? this.revision,
completionDate: completionDate ?? this.completionDate,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (revision.present) {
map['revision'] = Variable<int>(revision.value);
}
if (completionDate.present) {
map['completion_date'] = Variable<DateTime>(completionDate.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('OnboardingCompanion(')
..write('revision: $revision, ')
..write('completionDate: $completionDate, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$UserDatabase extends GeneratedDatabase {
_$UserDatabase(QueryExecutor e) : super(e);
$UserDatabaseManager get managers => $UserDatabaseManager(this);
late final Setting setting = Setting(this);
late final IconCache iconCache = IconCache(this);
late final Onboarding onboarding = Onboarding(this);
late final SettingDao settingDao = SettingDao(this as UserDatabase);
late final CacheDao cacheDao = CacheDao(this as UserDatabase);
late final OnboardingDao onboardingDao = OnboardingDao(this as UserDatabase);
Future<int> evictCacheEntries({required int limit}) {
return customUpdate(
'DELETE FROM icon_cache WHERE "rowid" IN (SELECT "rowid" FROM icon_cache ORDER BY fetch_date DESC LIMIT -1 OFFSET ?1)',
@@ -466,7 +652,11 @@ abstract class _$UserDatabase extends GeneratedDatabase {
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [setting, iconCache];
List<DatabaseSchemaEntity> get allSchemaEntities => [
setting,
iconCache,
onboarding,
];
}
typedef $SettingCreateCompanionBuilder =
@@ -780,6 +970,149 @@ typedef $IconCacheProcessedTableManager =
IconCacheData,
PrefetchHooks Function()
>;
typedef $OnboardingCreateCompanionBuilder =
OnboardingCompanion Function({
required int revision,
required DateTime completionDate,
Value<int> rowid,
});
typedef $OnboardingUpdateCompanionBuilder =
OnboardingCompanion Function({
Value<int> revision,
Value<DateTime> completionDate,
Value<int> rowid,
});
class $OnboardingFilterComposer extends Composer<_$UserDatabase, Onboarding> {
$OnboardingFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get revision => $composableBuilder(
column: $table.revision,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get completionDate => $composableBuilder(
column: $table.completionDate,
builder: (column) => ColumnFilters(column),
);
}
class $OnboardingOrderingComposer extends Composer<_$UserDatabase, Onboarding> {
$OnboardingOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get revision => $composableBuilder(
column: $table.revision,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get completionDate => $composableBuilder(
column: $table.completionDate,
builder: (column) => ColumnOrderings(column),
);
}
class $OnboardingAnnotationComposer
extends Composer<_$UserDatabase, Onboarding> {
$OnboardingAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get revision =>
$composableBuilder(column: $table.revision, builder: (column) => column);
GeneratedColumn<DateTime> get completionDate => $composableBuilder(
column: $table.completionDate,
builder: (column) => column,
);
}
class $OnboardingTableManager
extends
RootTableManager<
_$UserDatabase,
Onboarding,
OnboardingData,
$OnboardingFilterComposer,
$OnboardingOrderingComposer,
$OnboardingAnnotationComposer,
$OnboardingCreateCompanionBuilder,
$OnboardingUpdateCompanionBuilder,
(
OnboardingData,
BaseReferences<_$UserDatabase, Onboarding, OnboardingData>,
),
OnboardingData,
PrefetchHooks Function()
> {
$OnboardingTableManager(_$UserDatabase db, Onboarding table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$OnboardingFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$OnboardingOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$OnboardingAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<int> revision = const Value.absent(),
Value<DateTime> completionDate = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => OnboardingCompanion(
revision: revision,
completionDate: completionDate,
rowid: rowid,
),
createCompanionCallback:
({
required int revision,
required DateTime completionDate,
Value<int> rowid = const Value.absent(),
}) => OnboardingCompanion.insert(
revision: revision,
completionDate: completionDate,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $OnboardingProcessedTableManager =
ProcessedTableManager<
_$UserDatabase,
Onboarding,
OnboardingData,
$OnboardingFilterComposer,
$OnboardingOrderingComposer,
$OnboardingAnnotationComposer,
$OnboardingCreateCompanionBuilder,
$OnboardingUpdateCompanionBuilder,
(
OnboardingData,
BaseReferences<_$UserDatabase, Onboarding, OnboardingData>,
),
OnboardingData,
PrefetchHooks Function()
>;
class $UserDatabaseManager {
final _$UserDatabase _db;
@@ -787,4 +1120,6 @@ class $UserDatabaseManager {
$SettingTableManager get setting => $SettingTableManager(_db, _db.setting);
$IconCacheTableManager get iconCache =>
$IconCacheTableManager(_db, _db.iconCache);
$OnboardingTableManager get onboarding =>
$OnboardingTableManager(_db, _db.onboarding);
}
@@ -0,0 +1,34 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/user/data/providers.dart';
part 'onboarding.g.dart';
@Riverpod(keepAlive: true)
class OnboardingRepository extends _$OnboardingRepository {
static const targetRevision = 1;
Future<int?> getCurrentRevision() {
return ref
.read(userDatabaseProvider)
.onboardingDao
.getLastRevision()
.getSingleOrNull();
}
Future<void> pushRevision(int revision) {
return ref
.read(userDatabaseProvider)
.onboardingDao
.pushRevision(revision, DateTime.now());
}
Future<bool> isOutdated() async {
final current = await getCurrentRevision();
return current != targetRevision;
}
@override
void build() {
return;
}
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'onboarding.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$onboardingRepositoryHash() =>
r'37b76aa313ae6dfed6ab5695b4df8c4a0cfe0483';
/// See also [OnboardingRepository].
@ProviderFor(OnboardingRepository)
final onboardingRepositoryProvider =
NotifierProvider<OnboardingRepository, void>.internal(
OnboardingRepository.new,
name: r'onboardingRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$onboardingRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$OnboardingRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
+1 -1
View File
@@ -52,7 +52,7 @@ class MainApp extends HookConsumerWidget {
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
routerConfig: router,
routerConfig: router.valueOrNull,
);
},
onFailure: (errorMessage) {
+1
View File
@@ -57,6 +57,7 @@ dependencies:
path: ../packages/simple_intent_receiver
skeletonizer: ^2.0.1
sliver_tools: ^0.2.12
smooth_page_indicator: ^1.2.1
speech_to_text_google_dialog:
git:
url: https://github.com/FaFre/speech_to_text_google_dialog.git