From 00d0599dde1d43464553667d700027ecb9886b2f Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 27 Nov 2025 07:51:16 +0100 Subject: [PATCH] initial multi user feature --- app/lib/core/filesystem.dart | 175 ++++++++++++++++++ app/lib/core/http_error_handler.dart | 3 +- app/lib/core/providers/router.dart | 2 +- app/lib/core/providers/router.g.dart | 2 +- app/lib/core/routing/routes.browser.dart | 67 ++++++- app/lib/core/routing/routes.dart | 4 + app/lib/core/routing/routes.g.dart | 124 ++++++++++++- app/lib/domain/entities/profile.dart | 31 ++++ app/lib/domain/entities/profile.g.dart | 68 +++++++ app/lib/features/bangs/data/providers.dart | 19 +- app/lib/features/bangs/data/providers.g.dart | 2 +- .../features/geckoview/domain/providers.dart | 16 +- .../geckoview/domain/providers.g.dart | 8 +- .../browser_modules/bottom_app_bar.dart | 59 +++--- .../domain/providers/readerable.dart | 4 +- .../domain/providers/readerable.g.dart | 2 +- .../features/tabs/data/providers.dart | 19 +- .../features/tabs/data/providers.g.dart | 2 +- app/lib/features/user/data/providers.dart | 19 +- app/lib/features/user/data/providers.g.dart | 2 +- .../presentation/dialogs/select_profile.dart | 56 ++++++ .../presentation/screens/profile_edit.dart | 126 +++++++++++++ .../presentation/screens/profile_list.dart | 56 ++++++ app/lib/features/user/domain/providers.dart | 18 +- app/lib/features/user/domain/providers.g.dart | 66 +++---- .../user/domain/repositories/profile.dart | 57 ++++++ .../user/domain/repositories/profile.g.dart | 55 ++++++ app/lib/features/web_feed/data/providers.dart | 19 +- .../features/web_feed/data/providers.g.dart | 2 +- app/lib/main.dart | 4 + app/lib/utils/exit_app.dart | 20 ++ app/lib/utils/filesystem.dart | 130 +++++++++++++ app/pubspec.yaml | 2 - .../android/build.gradle | 2 +- .../BaseBrowserFragment.kt | 10 +- .../BrowserFragment.kt | 12 +- .../flutter_mozilla_components/Components.kt | 15 +- .../GlobalComponents.kt | 33 ++-- .../ProfileContext.kt | 127 +++++++++++++ .../api/GeckoBrowserApiImpl.kt | 79 +++++--- .../pigeons/Gecko.g.kt | 11 +- .../lib/src/domain/services/gecko_addon.dart | 10 +- .../src/domain/services/gecko_browser.dart | 8 +- .../lib/src/domain/services/gecko_event.dart | 36 ++-- .../src/domain/services/gecko_readerable.dart | 4 +- .../domain/services/gecko_suggestions.dart | 4 +- .../domain/services/gecko_tab_content.dart | 4 +- .../lib/src/pigeons/gecko.g.dart | 4 +- .../pigeons/gecko.dart | 1 + 49 files changed, 1341 insertions(+), 258 deletions(-) create mode 100644 app/lib/core/filesystem.dart create mode 100644 app/lib/domain/entities/profile.dart create mode 100644 app/lib/domain/entities/profile.g.dart create mode 100644 app/lib/features/user/domain/presentation/dialogs/select_profile.dart create mode 100644 app/lib/features/user/domain/presentation/screens/profile_edit.dart create mode 100644 app/lib/features/user/domain/presentation/screens/profile_list.dart create mode 100644 app/lib/features/user/domain/repositories/profile.dart create mode 100644 app/lib/features/user/domain/repositories/profile.g.dart create mode 100644 app/lib/utils/exit_app.dart create mode 100644 app/lib/utils/filesystem.dart create mode 100644 packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ProfileContext.kt diff --git a/app/lib/core/filesystem.dart b/app/lib/core/filesystem.dart new file mode 100644 index 00000000..b1b17711 --- /dev/null +++ b/app/lib/core/filesystem.dart @@ -0,0 +1,175 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:sqlite3/sqlite3.dart'; + +import 'package:uuid/uuid.dart'; +import 'package:weblibre/domain/entities/profile.dart'; +import 'package:weblibre/utils/filesystem.dart' as fs; + +final filesystem = _Filesystem(); + +class _Filesystem { + late final Directory dataDir; + late final Directory profilesDir; + + late final UuidValue selectedProfile; + late final Directory selectedProfileDir; + late final Directory profileDatabasesDir; + + late final String relativeProfilePath; + + Future> getAvailableProfileDirectories() { + return profilesDir.list().transform(fs.profileTransformer).toList(); + } + + Future readProfileMetadata(Directory profileDir) { + return fs.readProfileMetadata(profileDir); + } + + Directory getProfileDir(UuidValue uuid) { + return fs.getProfileDir(profilesDir, uuid); + } + + Future createNewProfile(Profile profile) { + return fs.createNewProfile(profilesDir, profile); + } + + Future updateProfileMetadata(Profile profile) { + return fs.writeProfileMetadata(getProfileDir(profile.uuidValue), profile); + } + + Future setStartupProfile(UuidValue profile) { + return fs.writeStartupProfile(profilesDir, profile, flush: true); + } + + Future _linkMozillaDir(Directory filesDir) async { + final mozillaDir = Directory(p.join(selectedProfileDir.path, 'mozilla')); + await mozillaDir.create(); + + final mozillaLink = Link(p.join(filesDir.path, 'mozilla')); + if (await mozillaLink.exists()) { + await mozillaLink.delete(); + } + + await mozillaLink.create(mozillaDir.path); + } + + Future _setupSqliteCache() async { + // Make sqlite3 pick a more suitable location for temporary files - the + // one from the system may be inaccessible due to sandboxing. + final cachebase = (await path_provider.getTemporaryDirectory()).path; + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cachebase; + } + + Future _copyDirectory( + Directory source, + Directory destination, + bool Function(FileSystemEntity e) filter, + ) async { + // Create destination directory + await destination.create(recursive: true); + + // List all contents + await for (final entity in source.list().where(filter)) { + final newPath = p.join(destination.path, p.basename(entity.path)); + + if (entity is Directory) { + // Recursively copy subdirectory + await _copyDirectory(entity, Directory(newPath), filter); + } else if (entity is File) { + // Copy file + await entity.copy(newPath); + } else if (entity is Link) { + // Copy link + await Link(newPath).create(await entity.target()); + } + } + } + + Future init() async { + final filesDir = await path_provider.getApplicationSupportDirectory(); + + dataDir = filesDir.parent; + + profilesDir = Directory(p.join(filesDir.path, fs.profilesDirName)); + await profilesDir.create(); + + final selectedProfile = await fs.selectStartupProfile(profilesDir); + if (selectedProfile == null) { + final defaultProfile = Profile.create(name: 'Default'); + if (!await fs.createNewProfile(profilesDir, defaultProfile)) { + throw Exception('Unable to create default profile'); + } + + this.selectedProfile = defaultProfile.uuidValue; + await fs.writeStartupProfile(profilesDir, defaultProfile.uuidValue); + + final mozillaDir = Directory(p.join(filesDir.path, 'mozilla')); + if (await mozillaDir.exists()) { + final type = await FileSystemEntity.type(mozillaDir.path); + if (type != FileSystemEntityType.link) { + await _migrate(defaultProfile, mozillaDir, filesDir); + } + } + } else { + this.selectedProfile = selectedProfile; + } + + relativeProfilePath = p.join( + fs.profilesDirName, + '${fs.profileDirPrefix}${this.selectedProfile.uuid}', + ); + selectedProfileDir = Directory(p.join(filesDir.path, relativeProfilePath)); + await selectedProfileDir.create(); + + profileDatabasesDir = Directory( + p.join(selectedProfileDir.path, 'databases'), + ); + await profileDatabasesDir.create(); + + await _linkMozillaDir(filesDir); + await _setupSqliteCache(); + } + + Future _migrate( + Profile defaultProfile, + Directory mozillaDir, + Directory filesDir, + ) async { + final profileDir = getProfileDir(defaultProfile.uuidValue); + + final newMozillaDir = Directory(p.join(profileDir.path, 'mozilla')); + await newMozillaDir.create(); + await mozillaDir.rename(newMozillaDir.path); + + await _copyDirectory( + filesDir, + Directory(p.join(profileDir.path, 'files')), + (e) => e is! Directory || p.basename(e.path) != fs.profilesDirName, + ); + + final profileDatabasesDir = Directory(p.join(profileDir.path, 'databases')); + + await _copyDirectory( + Directory(p.join(dataDir.path, 'databases')), + profileDatabasesDir, + (e) => true, + ); + + final dbFolder = await path_provider.getApplicationDocumentsDirectory(); + + final bangDb = File(p.join(dbFolder.path, 'bang3.db')); + await bangDb.copy(p.join(profileDatabasesDir.path, 'bang.db')); + final feedDb = File(p.join(dbFolder.path, 'feed.db')); + await feedDb.copy(p.join(profileDatabasesDir.path, 'feed.db')); + final tabDb = File(p.join(dbFolder.path, 'tab2.db')); + await tabDb.copy(p.join(profileDatabasesDir.path, 'tab.db')); + final userDb = File(p.join(dbFolder.path, 'user.db')); + await userDb.copy(p.join(profileDatabasesDir.path, 'user.db')); + } +} diff --git a/app/lib/core/http_error_handler.dart b/app/lib/core/http_error_handler.dart index 5be37c45..0c3b4eda 100644 --- a/app/lib/core/http_error_handler.dart +++ b/app/lib/core/http_error_handler.dart @@ -17,9 +17,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:io'; + import 'package:exceptions/exceptions.dart'; import 'package:http/http.dart'; -import 'package:universal_io/io.dart'; ErrorMessage handleHttpError(Exception exception, StackTrace stackTrace) { return switch (exception) { diff --git a/app/lib/core/providers/router.dart b/app/lib/core/providers/router.dart index 5631be7e..2aa69b9b 100644 --- a/app/lib/core/providers/router.dart +++ b/app/lib/core/providers/router.dart @@ -49,6 +49,6 @@ Future router(Ref ref) async { return GoRouter( debugLogDiagnostics: true, routes: $appRoutes, - initialLocation: initialLocation ?? BrowserRoute().location, + initialLocation: initialLocation ?? const BrowserRoute().location, ); } diff --git a/app/lib/core/providers/router.g.dart b/app/lib/core/providers/router.g.dart index 18f82e53..dfa4ba6c 100644 --- a/app/lib/core/providers/router.g.dart +++ b/app/lib/core/providers/router.g.dart @@ -41,4 +41,4 @@ final class RouterProvider } } -String _$routerHash() => r'ab1d1e2ea27dd41fe78d7430bfeba87051d88d36'; +String _$routerHash() => r'cbaa7e982114942303574f9573f4a75b79955583'; diff --git a/app/lib/core/routing/routes.browser.dart b/app/lib/core/routing/routes.browser.dart index be2a0368..d143f198 100644 --- a/app/lib/core/routing/routes.browser.dart +++ b/app/lib/core/routing/routes.browser.dart @@ -64,11 +64,28 @@ part of 'routes.dart'; name: 'OpenSharedContentRoute', path: 'open_content', ), + TypedGoRoute( + name: 'SelectProfileRoute', + path: 'profile', + ), + TypedGoRoute( + name: 'ProfileListRoute', + path: 'profiles', + routes: [ + TypedGoRoute(name: 'ProfileEditScreen', path: 'edit'), + TypedGoRoute( + name: 'CreateProfileRoute', + path: 'create', + ), + ], + ), ], ) class BrowserRoute extends GoRouteData with $BrowserRoute { static const name = 'BrowserRoute'; + const BrowserRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const BrowserScreen(); @@ -106,6 +123,8 @@ class SearchRoute extends GoRouteData with $SearchRoute { } class TorProxyRoute extends GoRouteData with $TorProxyRoute { + const TorProxyRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const TorProxyScreen(); @@ -113,6 +132,8 @@ class TorProxyRoute extends GoRouteData with $TorProxyRoute { } class ContainerDraftRoute extends GoRouteData with $ContainerDraftRoute { + const ContainerDraftRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const ContainerDraftSuggestionsScreen(); @@ -120,6 +141,8 @@ class ContainerDraftRoute extends GoRouteData with $ContainerDraftRoute { } class ContainerListRoute extends GoRouteData with $ContainerListRoute { + const ContainerListRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const ContainerListScreen(); @@ -128,6 +151,8 @@ class ContainerListRoute extends GoRouteData with $ContainerListRoute { class ContainerSelectionRoute extends GoRouteData with $ContainerSelectionRoute { + const ContainerSelectionRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const ContainerSelectionScreen(); @@ -137,7 +162,7 @@ class ContainerSelectionRoute extends GoRouteData class ContainerEditRoute extends GoRouteData with $ContainerEditRoute { final String containerData; - ContainerEditRoute({required this.containerData}); + const ContainerEditRoute({required this.containerData}); @override Widget build(BuildContext context, GoRouterState state) { @@ -205,6 +230,8 @@ class OpenSharedContentRoute extends GoRouteData with $OpenSharedContentRoute { } class HistoryRoute extends GoRouteData with $HistoryRoute { + const HistoryRoute(); + @override Widget build(BuildContext context, GoRouterState state) { return const HistoryScreen(); @@ -212,8 +239,46 @@ class HistoryRoute extends GoRouteData with $HistoryRoute { } class TabViewRoute extends GoRouteData with $TabViewRoute { + const TabViewRoute(); + @override Page buildPage(BuildContext context, GoRouterState state) { return DialogPage(builder: (_) => const TabViewScreen()); } } + +class SelectProfileRoute extends GoRouteData with $SelectProfileRoute { + const SelectProfileRoute(); + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return DialogPage(builder: (_) => SelectProfileDialog()); + } +} + +class ProfileListRoute extends GoRouteData with $ProfileListRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProfileListScreen(); + } +} + +class CreateProfileRoute extends GoRouteData with $CreateProfileRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProfileEditScreen(profile: null); + } +} + +class EditProfileRoute extends GoRouteData with $EditProfileRoute { + final String profile; + + const EditProfileRoute({required this.profile}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return ProfileEditScreen( + profile: Profile.fromJson(jsonDecode(profile) as Map), + ); + } +} diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index c29cca79..4f3c3f85 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/widgets/dialog_page.dart'; +import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/features/about/presentation/screens/about.dart'; import 'package:weblibre/features/bangs/data/models/bang.dart'; import 'package:weblibre/features/bangs/presentation/screens/categories.dart'; @@ -57,6 +58,9 @@ import 'package:weblibre/features/settings/presentation/screens/web_engine_harde import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_settings.dart'; import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart'; +import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart'; +import 'package:weblibre/features/user/domain/presentation/screens/profile_edit.dart'; +import 'package:weblibre/features/user/domain/presentation/screens/profile_list.dart'; import 'package:weblibre/features/web_feed/presentation/add_feed_dialog.dart'; import 'package:weblibre/features/web_feed/presentation/screens/feed_article.dart'; import 'package:weblibre/features/web_feed/presentation/screens/feed_article_list.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 9aa8ea75..9e616a96 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -442,11 +442,33 @@ RouteBase get $browserRoute => GoRouteData.$route( name: 'OpenSharedContentRoute', factory: $OpenSharedContentRoute._fromState, ), + GoRouteData.$route( + path: 'profile', + name: 'SelectProfileRoute', + factory: $SelectProfileRoute._fromState, + ), + GoRouteData.$route( + path: 'profiles', + name: 'ProfileListRoute', + factory: $ProfileListRoute._fromState, + routes: [ + GoRouteData.$route( + path: 'edit', + name: 'ProfileEditScreen', + factory: $EditProfileRoute._fromState, + ), + GoRouteData.$route( + path: 'create', + name: 'CreateProfileRoute', + factory: $CreateProfileRoute._fromState, + ), + ], + ), ], ); mixin $BrowserRoute on GoRouteData { - static BrowserRoute _fromState(GoRouterState state) => BrowserRoute(); + static BrowserRoute _fromState(GoRouterState state) => const BrowserRoute(); @override String get location => GoRouteData.$location('/browser'); @@ -511,7 +533,7 @@ const _$TabTypeEnumMap = { }; mixin $TorProxyRoute on GoRouteData { - static TorProxyRoute _fromState(GoRouterState state) => TorProxyRoute(); + static TorProxyRoute _fromState(GoRouterState state) => const TorProxyRoute(); @override String get location => GoRouteData.$location('/browser/tor_proxy'); @@ -531,7 +553,7 @@ mixin $TorProxyRoute on GoRouteData { } mixin $HistoryRoute on GoRouteData { - static HistoryRoute _fromState(GoRouterState state) => HistoryRoute(); + static HistoryRoute _fromState(GoRouterState state) => const HistoryRoute(); @override String get location => GoRouteData.$location('/browser/history'); @@ -551,7 +573,7 @@ mixin $HistoryRoute on GoRouteData { } mixin $TabViewRoute on GoRouteData { - static TabViewRoute _fromState(GoRouterState state) => TabViewRoute(); + static TabViewRoute _fromState(GoRouterState state) => const TabViewRoute(); @override String get location => GoRouteData.$location('/browser/tab_view'); @@ -598,7 +620,7 @@ mixin $ContextMenuRoute on GoRouteData { mixin $ContainerDraftRoute on GoRouteData { static ContainerDraftRoute _fromState(GoRouterState state) => - ContainerDraftRoute(); + const ContainerDraftRoute(); @override String get location => GoRouteData.$location('/browser/container_draft'); @@ -619,7 +641,7 @@ mixin $ContainerDraftRoute on GoRouteData { mixin $ContainerListRoute on GoRouteData { static ContainerListRoute _fromState(GoRouterState state) => - ContainerListRoute(); + const ContainerListRoute(); @override String get location => GoRouteData.$location('/browser/containers'); @@ -692,7 +714,7 @@ mixin $ContainerEditRoute on GoRouteData { mixin $ContainerSelectionRoute on GoRouteData { static ContainerSelectionRoute _fromState(GoRouterState state) => - ContainerSelectionRoute(); + const ContainerSelectionRoute(); @override String get location => GoRouteData.$location('/browser/select_container'); @@ -766,6 +788,94 @@ mixin $OpenSharedContentRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $SelectProfileRoute on GoRouteData { + static SelectProfileRoute _fromState(GoRouterState state) => + const SelectProfileRoute(); + + @override + String get location => GoRouteData.$location('/browser/profile'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $ProfileListRoute on GoRouteData { + static ProfileListRoute _fromState(GoRouterState state) => ProfileListRoute(); + + @override + String get location => GoRouteData.$location('/browser/profiles'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $EditProfileRoute on GoRouteData { + static EditProfileRoute _fromState(GoRouterState state) => + EditProfileRoute(profile: state.uri.queryParameters['profile']!); + + EditProfileRoute get _self => this as EditProfileRoute; + + @override + String get location => GoRouteData.$location( + '/browser/profiles/edit', + queryParams: {'profile': _self.profile}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $CreateProfileRoute on GoRouteData { + static CreateProfileRoute _fromState(GoRouterState state) => + CreateProfileRoute(); + + @override + String get location => GoRouteData.$location('/browser/profiles/create'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + T? _$convertMapValue( String key, Map map, diff --git a/app/lib/domain/entities/profile.dart b/app/lib/domain/entities/profile.dart new file mode 100644 index 00000000..fca512a3 --- /dev/null +++ b/app/lib/domain/entities/profile.dart @@ -0,0 +1,31 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:uuid/uuid_value.dart'; +import 'package:weblibre/core/uuid.dart'; + +part 'profile.g.dart'; + +@JsonSerializable() +@CopyWith() +class Profile with FastEquatable { + @CopyWithField(immutable: true) + final String id; + final String name; + + late final uuidValue = UuidValue.fromString(id); + + Profile({required this.id, required this.name}); + + factory Profile.create({required String name}) { + return Profile(id: uuid.v7(), name: name); + } + + @override + List get hashParameters => [id, name]; + + factory Profile.fromJson(Map json) => + _$ProfileFromJson(json); + + Map toJson() => _$ProfileToJson(this); +} diff --git a/app/lib/domain/entities/profile.g.dart b/app/lib/domain/entities/profile.g.dart new file mode 100644 index 00000000..646459ca --- /dev/null +++ b/app/lib/domain/entities/profile.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'profile.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ProfileCWProxy { + Profile name(String name); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// Profile(...).copyWith(id: 12, name: "My name") + /// ``` + Profile call({String name}); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfProfile.copyWith(...)` or call `instanceOfProfile.copyWith.fieldName(value)` for a single field. +class _$ProfileCWProxyImpl implements _$ProfileCWProxy { + const _$ProfileCWProxyImpl(this._value); + + final Profile _value; + + @override + Profile name(String name) => call(name: name); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// Profile(...).copyWith(id: 12, name: "My name") + /// ``` + Profile call({Object? name = const $CopyWithPlaceholder()}) { + return Profile( + id: _value.id, + name: name == const $CopyWithPlaceholder() || name == null + ? _value.name + // ignore: cast_nullable_to_non_nullable + : name as String, + ); + } +} + +extension $ProfileCopyWith on Profile { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfProfile.copyWith(...)` or `instanceOfProfile.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ProfileCWProxy get copyWith => _$ProfileCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Profile _$ProfileFromJson(Map json) => + Profile(id: json['id'] as String, name: json['name'] as String); + +Map _$ProfileToJson(Profile instance) => { + 'id': instance.id, + 'name': instance.name, +}; diff --git a/app/lib/features/bangs/data/providers.dart b/app/lib/features/bangs/data/providers.dart index 4f925dd4..ca215676 100644 --- a/app/lib/features/bangs/data/providers.dart +++ b/app/lib/features/bangs/data/providers.dart @@ -17,14 +17,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; -import 'package:universal_io/io.dart'; + +import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/bangs/data/database/database.dart'; part 'providers.g.dart'; @@ -33,23 +34,13 @@ part 'providers.g.dart'; BangDatabase bangDatabase(Ref ref) { final db = BangDatabase( LazyDatabase(() async { - // put the database file, called db.sqlite here, into the documents folder - // for your app. - final dbFolder = await path_provider.getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'bang3.db')); + final file = File(p.join(filesystem.profileDatabasesDir.path, 'bang.db')); // Also work around limitations on old Android versions if (Platform.isAndroid) { await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); } - // Make sqlite3 pick a more suitable location for temporary files - the - // one from the system may be inaccessible due to sandboxing. - final cachebase = (await path_provider.getTemporaryDirectory()).path; - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; - return NativeDatabase.createInBackground(file); }), ); diff --git a/app/lib/features/bangs/data/providers.g.dart b/app/lib/features/bangs/data/providers.g.dart index 89b2f3b5..5df1c9fd 100644 --- a/app/lib/features/bangs/data/providers.g.dart +++ b/app/lib/features/bangs/data/providers.g.dart @@ -48,4 +48,4 @@ final class BangDatabaseProvider } } -String _$bangDatabaseHash() => r'e46ec1582f1a21337a302bef1b7963c2e11468d9'; +String _$bangDatabaseHash() => r'86fed6bcc4a1e8a0621869c2886b1b80d353f14f'; diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart index 1eb40061..9bfe5ba1 100644 --- a/app/lib/features/geckoview/domain/providers.dart +++ b/app/lib/features/geckoview/domain/providers.dart @@ -130,8 +130,8 @@ GeckoSelectionActionService selectionActionService(Ref ref) { GeckoEventService eventService(Ref ref) { final service = GeckoEventService.setUp(); - ref.onDispose(() { - service.dispose(); + ref.onDispose(() async { + await service.dispose(); }); return service; @@ -141,8 +141,8 @@ GeckoEventService eventService(Ref ref) { GeckoAddonService addonService(Ref ref) { final service = GeckoAddonService.setUp(); - ref.onDispose(() { - service.dispose(); + ref.onDispose(() async { + await service.dispose(); }); return service; @@ -152,8 +152,8 @@ GeckoAddonService addonService(Ref ref) { GeckoTabContentService tabContentService(Ref ref) { final service = GeckoTabContentService.setUp(); - ref.onDispose(() { - service.dispose(); + ref.onDispose(() async { + await service.dispose(); }); return service; @@ -163,8 +163,8 @@ GeckoTabContentService tabContentService(Ref ref) { GeckoSuggestionsService engineSuggestionsService(Ref ref) { final service = GeckoSuggestionsService.setUp(); - ref.onDispose(() { - service.dispose(); + ref.onDispose(() async { + await service.dispose(); }); return service; diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart index dccdb1ff..feedd47a 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -102,7 +102,7 @@ final class EventServiceProvider } } -String _$eventServiceHash() => r'166b01f636fbdd4355dbc55a18ca4f83e0006de8'; +String _$eventServiceHash() => r'3a297348fadda05dc60433d7ce8f662b2ff62c26'; @ProviderFor(addonService) const addonServiceProvider = AddonServiceProvider._(); @@ -149,7 +149,7 @@ final class AddonServiceProvider } } -String _$addonServiceHash() => r'c7aca09b99c3810908176f3464f5f90a185aa5e7'; +String _$addonServiceHash() => r'30fedb35c68943159246df79b5f1b62a25767fa0'; @ProviderFor(tabContentService) const tabContentServiceProvider = TabContentServiceProvider._(); @@ -196,7 +196,7 @@ final class TabContentServiceProvider } } -String _$tabContentServiceHash() => r'd9a991add907ecc138c62790883e59d8e9aa9266'; +String _$tabContentServiceHash() => r'12d8322c37ded4ad3344af327d884bbf7f089594'; @ProviderFor(engineSuggestionsService) const engineSuggestionsServiceProvider = EngineSuggestionsServiceProvider._(); @@ -244,7 +244,7 @@ final class EngineSuggestionsServiceProvider } String _$engineSuggestionsServiceHash() => - r'f7414b335564578b2c7f6a86baf8bf13f2d5ba2d'; + r'1ec1192f0c5c86cecc7ad448ee2b039f7a48e32b'; @ProviderFor(EngineReadyState) const engineReadyStateProvider = EngineReadyStateProvider._(); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 45844a7f..ddcfda19 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -24,7 +24,6 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:weblibre/core/providers/defaults.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart'; @@ -45,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/readerview/presentation/con import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; +import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; @@ -236,7 +236,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { .show(ViewTabsSheet()); } } else { - await TabViewRoute().push(context); + await const TabViewRoute().push(context); } }, onLongPress: () { @@ -272,6 +272,19 @@ class BrowserBottomAppBar extends HookConsumerWidget { ); }, menuChildren: [ + MenuItemButton( + onPressed: () async { + await const SelectProfileRoute().push(context); + }, + leadingIcon: const Icon(Icons.person), + child: Consumer( + builder: (context, ref, child) { + final profile = ref.watch(selectedProfileProvider); + return Text(profile.value?.name ?? 'Profile'); + }, + ), + ), + const Divider(), Consumer( builder: (context, childRef, child) { final pageExtensions = childRef.watch( @@ -309,25 +322,25 @@ class BrowserBottomAppBar extends HookConsumerWidget { leadingIcon: const Icon(Icons.info), child: const Text('About'), ), - MenuItemButton( - onPressed: () async { - final isPrivate = - ref - .read(generalSettingsWithDefaultsProvider) - .defaultCreateTabType == - TabType.private; + // MenuItemButton( + // onPressed: () async { + // final isPrivate = + // ref + // .read(generalSettingsWithDefaultsProvider) + // .defaultCreateTabType == + // TabType.private; - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: ref.read(docsUriProvider), - private: isPrivate, - container: const Value(null), - ); - }, - leadingIcon: const Icon(Icons.help), - child: const Text('Help and feedback'), - ), + // await ref + // .read(tabRepositoryProvider.notifier) + // .addTab( + // url: ref.read(docsUriProvider), + // private: isPrivate, + // container: const Value(null), + // ); + // }, + // leadingIcon: const Icon(Icons.help), + // child: const Text('Help and feedback'), + // ), const Divider(), MenuItemButton( onPressed: () async { @@ -338,7 +351,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ), MenuItemButton( onPressed: () async { - await HistoryRoute().push(context); + await const HistoryRoute().push(context); }, leadingIcon: const Icon(Icons.history), child: const Text('History'), @@ -403,7 +416,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ), MenuItemButton( onPressed: () async { - await TorProxyRoute().push(context); + await const TorProxyRoute().push(context); }, leadingIcon: const Icon(TorIcons.onionAlt), child: Consumer( @@ -433,7 +446,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { ), MenuItemButton( onPressed: () async { - await ContainerListRoute().push(context); + await const ContainerListRoute().push(context); }, leadingIcon: const Icon(MdiIcons.folder), child: const Text('Containers'), diff --git a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart index 9d370b24..75bf0812 100644 --- a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart @@ -26,8 +26,8 @@ part 'readerable.g.dart'; GeckoReaderableService readerableService(Ref ref) { final service = GeckoReaderableService.setUp(); - ref.onDispose(() { - service.dispose(); + ref.onDispose(() async { + await service.dispose(); }); return service; diff --git a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart index 9ae5d51c..0bc6d394 100644 --- a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart @@ -54,7 +54,7 @@ final class ReaderableServiceProvider } } -String _$readerableServiceHash() => r'03182c8afc41f5184322a3206473cbfa96df5819'; +String _$readerableServiceHash() => r'0c432ede496d85ed6a7d028af346b129d97f7502'; @ProviderFor(appearanceButtonVisibility) const appearanceButtonVisibilityProvider = diff --git a/app/lib/features/geckoview/features/tabs/data/providers.dart b/app/lib/features/geckoview/features/tabs/data/providers.dart index e042e6fa..cc180f43 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.dart @@ -17,14 +17,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; -import 'package:universal_io/io.dart'; + +import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; @@ -34,23 +35,13 @@ part 'providers.g.dart'; TabDatabase tabDatabase(Ref ref) { final db = TabDatabase( LazyDatabase(() async { - // put the database file, called db.sqlite here, into the documents folder - // for your app. - final dbFolder = await path_provider.getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'tab2.db')); + final file = File(p.join(filesystem.profileDatabasesDir.path, 'tab.db')); // Also work around limitations on old Android versions if (Platform.isAndroid) { await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); } - // Make sqlite3 pick a more suitable location for temporary files - the - // one from the system may be inaccessible due to sandboxing. - final cachebase = (await path_provider.getTemporaryDirectory()).path; - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; - return NativeDatabase.createInBackground( file, setup: (database) { diff --git a/app/lib/features/geckoview/features/tabs/data/providers.g.dart b/app/lib/features/geckoview/features/tabs/data/providers.g.dart index 30c0c1d3..fadbaef8 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.g.dart @@ -48,4 +48,4 @@ final class TabDatabaseProvider } } -String _$tabDatabaseHash() => r'422bd4789296dc271fabbd5906f2e2ab16bccaa3'; +String _$tabDatabaseHash() => r'337dbcf30bd57dcee409aec0aa93f1f6f2783368'; diff --git a/app/lib/features/user/data/providers.dart b/app/lib/features/user/data/providers.dart index 8d04ace6..ff5907bf 100644 --- a/app/lib/features/user/data/providers.dart +++ b/app/lib/features/user/data/providers.dart @@ -17,15 +17,16 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; -import 'package:universal_io/io.dart'; + +import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/user/data/database/database.dart'; import 'package:weblibre/features/user/data/database/riverpod_storage.dart'; @@ -35,23 +36,13 @@ part 'providers.g.dart'; UserDatabase userDatabase(Ref ref) { final db = UserDatabase( LazyDatabase(() async { - // put the database file, called db.sqlite here, into the documents folder - // for your app. - final dbFolder = await path_provider.getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'user.db')); + final file = File(p.join(filesystem.profileDatabasesDir.path, 'user.db')); // Also work around limitations on old Android versions if (Platform.isAndroid) { await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); } - // Make sqlite3 pick a more suitable location for temporary files - the - // one from the system may be inaccessible due to sandboxing. - final cachebase = (await path_provider.getTemporaryDirectory()).path; - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; - return NativeDatabase.createInBackground(file); }), ); diff --git a/app/lib/features/user/data/providers.g.dart b/app/lib/features/user/data/providers.g.dart index 426de194..77e4c5a8 100644 --- a/app/lib/features/user/data/providers.g.dart +++ b/app/lib/features/user/data/providers.g.dart @@ -48,7 +48,7 @@ final class UserDatabaseProvider } } -String _$userDatabaseHash() => r'b925780435806f0241d7ddb635e301e9fc8baf6e'; +String _$userDatabaseHash() => r'8cc40197f9dbb85ccd8263984218753d3690954b'; @ProviderFor(riverpodDatabaseStorage) const riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._(); diff --git a/app/lib/features/user/domain/presentation/dialogs/select_profile.dart b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart new file mode 100644 index 00000000..92ac4c44 --- /dev/null +++ b/app/lib/features/user/domain/presentation/dialogs/select_profile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/filesystem.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/user/domain/repositories/profile.dart'; +import 'package:weblibre/presentation/widgets/failure_widget.dart'; +import 'package:weblibre/utils/exit_app.dart'; + +class SelectProfileDialog extends HookConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final usersAsync = ref.watch(profileRepositoryProvider); + + return AlertDialog( + title: const Text('Manage Users'), + scrollable: true, + content: usersAsync.when( + data: (profiles) => Column( + children: profiles.map((profile) { + final isSelected = filesystem.selectedProfile == profile.uuidValue; + + return ListTile( + key: ValueKey(profile.id), + enabled: !isSelected, + leading: const Icon(Icons.person), + title: Text(profile.name), + subtitle: isSelected ? const Text('Active') : null, + onTap: () async { + await ref + .read(profileRepositoryProvider.notifier) + .switchProfile(profile.id); + await exitApp(ref.container); + }, + ); + }).toList(), + ), + error: (error, stackTrace) => Center( + child: FailureWidget( + title: 'Failed to load Profiles', + exception: error, + ), + ), + loading: () => const Center(child: CircularProgressIndicator()), + ), + actions: [ + TextButton.icon( + icon: const Icon(Icons.edit), + label: const Text('Edit'), + onPressed: () async { + await ProfileListRoute().push(context); + }, + ), + ], + ); + } +} diff --git a/app/lib/features/user/domain/presentation/screens/profile_edit.dart b/app/lib/features/user/domain/presentation/screens/profile_edit.dart new file mode 100644 index 00000000..7feb0115 --- /dev/null +++ b/app/lib/features/user/domain/presentation/screens/profile_edit.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/domain/entities/profile.dart'; +import 'package:weblibre/features/user/domain/repositories/profile.dart'; +import 'package:weblibre/utils/form_validators.dart'; + +class ProfileEditScreen extends HookConsumerWidget { + final Profile? profile; + + const ProfileEditScreen({required this.profile}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final nameTextController = useTextEditingController(text: profile?.name); + + return Scaffold( + appBar: AppBar( + title: (profile != null) + ? const Text('Edit User') + : const Text('Create User'), + actions: [ + IconButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + if (profile != null) { + await ref + .read(profileRepositoryProvider.notifier) + .updateProfileMetadata( + profile!.copyWith.name(nameTextController.text), + ); + + if (context.mounted) { + context.pop(); + } + } else { + await ref + .read(profileRepositoryProvider.notifier) + .createProfile(name: nameTextController.text); + + if (context.mounted) { + context.pop(); + } + } + } + }, + icon: const Icon(Icons.check), + ), + ], + ), + body: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: ListView( + children: [ + TextFormField( + controller: nameTextController, + decoration: const InputDecoration( + label: Text('Name'), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + validator: validateRequired, + ), + const SizedBox(height: 16), + if (profile != null) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + foregroundColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + label: const Text('Delete'), + icon: const Icon(Icons.delete), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete User'), + content: const Text( + 'Are you sure you want to delete this User including all data?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (result == true) { + await ref + .read(profileRepositoryProvider.notifier) + .deleteProfile(profile!.uuidValue.uuid); + + if (context.mounted) { + context.pop(); + } + } + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/features/user/domain/presentation/screens/profile_list.dart b/app/lib/features/user/domain/presentation/screens/profile_list.dart new file mode 100644 index 00000000..2a81253e --- /dev/null +++ b/app/lib/features/user/domain/presentation/screens/profile_list.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/filesystem.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/user/domain/repositories/profile.dart'; +import 'package:weblibre/presentation/widgets/failure_widget.dart'; + +class ProfileListScreen extends HookConsumerWidget { + const ProfileListScreen(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usersAsync = ref.watch(profileRepositoryProvider); + + return Scaffold( + appBar: AppBar(title: const Text('Profiles')), + body: usersAsync.when( + data: (profiles) => ListView.builder( + itemCount: profiles.length, + itemBuilder: (context, index) { + final profile = profiles[index]; + final isSelected = filesystem.selectedProfile == profile.uuidValue; + + return ListTile( + enabled: !isSelected, + leading: const Icon(Icons.person), + title: Text(profile.name), + subtitle: isSelected ? const Text('Active') : null, + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await EditProfileRoute( + profile: jsonEncode(profile.toJson()), + ).push(context); + }, + ); + }, + ), + error: (error, stackTrace) => Center( + child: FailureWidget( + title: 'Failed to load Profiles', + exception: error, + ), + ), + loading: () => const Center(child: CircularProgressIndicator()), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await CreateProfileRoute().push(context); + }, + child: const Icon(Icons.person_add), + ), + ); + } +} diff --git a/app/lib/features/user/domain/providers.dart b/app/lib/features/user/domain/providers.dart index a6ff8018..57eb343a 100644 --- a/app/lib/features/user/domain/providers.dart +++ b/app/lib/features/user/domain/providers.dart @@ -18,26 +18,20 @@ * along with this program. If not, see . */ import 'package:exceptions/exceptions.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:nullability/nullability.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/filesystem.dart'; +import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/features/user/data/providers.dart'; import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/profile.dart'; import 'package:weblibre/features/user/domain/services/fingerprinting.dart'; part 'providers.g.dart'; -const _authKey = 'pb_auth'; - -@Riverpod() -Future _storedAuthData(Ref ref) { - const secureStorage = FlutterSecureStorage(); - return secureStorage.read(key: _authKey); -} - @Riverpod() Stream iconCacheSizeMegabytes(Ref ref) { final repository = ref.watch(userDatabaseProvider); @@ -73,3 +67,9 @@ Future> fingerprintOverrideSettings( return overrides; } + +@Riverpod(keepAlive: true) +Future selectedProfile(Ref ref) async { + final profiles = await ref.watch(profileRepositoryProvider.future); + return profiles.firstWhere((p) => p.uuidValue == filesystem.selectedProfile); +} diff --git a/app/lib/features/user/domain/providers.g.dart b/app/lib/features/user/domain/providers.g.dart index e8a5bf8e..5273b688 100644 --- a/app/lib/features/user/domain/providers.g.dart +++ b/app/lib/features/user/domain/providers.g.dart @@ -9,39 +9,6 @@ part of 'providers.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -@ProviderFor(_storedAuthData) -const _storedAuthDataProvider = _StoredAuthDataProvider._(); - -final class _StoredAuthDataProvider - extends $FunctionalProvider, String?, FutureOr> - with $FutureModifier, $FutureProvider { - const _StoredAuthDataProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'_storedAuthDataProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$_storedAuthDataHash(); - - @$internal - @override - $FutureProviderElement $createElement($ProviderPointer pointer) => - $FutureProviderElement(pointer); - - @override - FutureOr create(Ref ref) { - return _storedAuthData(ref); - } -} - -String _$_storedAuthDataHash() => r'5f7e3ef6233a2036f7ce3728131901a46b1e548e'; - @ProviderFor(iconCacheSizeMegabytes) const iconCacheSizeMegabytesProvider = IconCacheSizeMegabytesProvider._(); @@ -160,3 +127,36 @@ final class FingerprintOverrideSettingsProvider String _$fingerprintOverrideSettingsHash() => r'd4d40ec425098fb1f5a2f0c4944f058829a41a0a'; + +@ProviderFor(selectedProfile) +const selectedProfileProvider = SelectedProfileProvider._(); + +final class SelectedProfileProvider + extends $FunctionalProvider, Profile, FutureOr> + with $FutureModifier, $FutureProvider { + const SelectedProfileProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'selectedProfileProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$selectedProfileHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return selectedProfile(ref); + } +} + +String _$selectedProfileHash() => r'c703cad8f30abb4f5f42db0119756ee6791ac477'; diff --git a/app/lib/features/user/domain/repositories/profile.dart b/app/lib/features/user/domain/repositories/profile.dart new file mode 100644 index 00000000..5cc371f6 --- /dev/null +++ b/app/lib/features/user/domain/repositories/profile.dart @@ -0,0 +1,57 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:uuid/uuid.dart'; +import 'package:weblibre/core/filesystem.dart'; + +import 'package:weblibre/domain/entities/profile.dart'; + +part 'profile.g.dart'; + +@Riverpod(keepAlive: true) +class ProfileRepository extends _$ProfileRepository { + Future> _readProfiles() { + return filesystem.getAvailableProfileDirectories().then((dirs) async { + final profiles = await Future.wait( + dirs.map(filesystem.readProfileMetadata), + ); + return profiles.nonNulls.toList(); + }); + } + + Future switchProfile(String id) async { + await filesystem.setStartupProfile(UuidValue.withValidation(id)); + } + + Future createProfile({required String name}) async { + final profile = Profile.create(name: name); + if (!await filesystem.createNewProfile(profile)) { + throw Exception('Could not create profile'); + } + + state = await AsyncValue.guard(_readProfiles); + + return profile; + } + + Future updateProfileMetadata(Profile profile) async { + await filesystem.updateProfileMetadata(profile); + state = await AsyncValue.guard(_readProfiles); + } + + Future deleteProfile(String id) async { + final uuid = UuidValue.withValidation(id); + if (filesystem.selectedProfile == uuid) { + return false; + } + + await filesystem.getProfileDir(uuid).delete(recursive: true); + + state = await AsyncValue.guard(_readProfiles); + + return true; + } + + @override + Future> build() { + return _readProfiles(); + } +} diff --git a/app/lib/features/user/domain/repositories/profile.g.dart b/app/lib/features/user/domain/repositories/profile.g.dart new file mode 100644 index 00000000..48224726 --- /dev/null +++ b/app/lib/features/user/domain/repositories/profile.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'profile.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ProfileRepository) +const profileRepositoryProvider = ProfileRepositoryProvider._(); + +final class ProfileRepositoryProvider + extends $AsyncNotifierProvider> { + const ProfileRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'profileRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$profileRepositoryHash(); + + @$internal + @override + ProfileRepository create() => ProfileRepository(); +} + +String _$profileRepositoryHash() => r'1357d42738d40e8e447ab8879292e81ad7b80b61'; + +abstract class _$ProfileRepository extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/app/lib/features/web_feed/data/providers.dart b/app/lib/features/web_feed/data/providers.dart index 6ef10947..0df189ba 100644 --- a/app/lib/features/web_feed/data/providers.dart +++ b/app/lib/features/web_feed/data/providers.dart @@ -17,14 +17,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; -import 'package:universal_io/io.dart'; + +import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/web_feed/data/database/database.dart'; part 'providers.g.dart'; @@ -33,23 +34,13 @@ part 'providers.g.dart'; FeedDatabase feedDatabase(Ref ref) { final db = FeedDatabase( LazyDatabase(() async { - // put the database file, called db.sqlite here, into the documents folder - // for your app. - final dbFolder = await path_provider.getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'feed.db')); + final file = File(p.join(filesystem.profileDatabasesDir.path, 'feed.db')); // Also work around limitations on old Android versions if (Platform.isAndroid) { await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); } - // Make sqlite3 pick a more suitable location for temporary files - the - // one from the system may be inaccessible due to sandboxing. - final cachebase = (await path_provider.getTemporaryDirectory()).path; - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; - return NativeDatabase.createInBackground(file); }), ); diff --git a/app/lib/features/web_feed/data/providers.g.dart b/app/lib/features/web_feed/data/providers.g.dart index 5e025349..efecf711 100644 --- a/app/lib/features/web_feed/data/providers.g.dart +++ b/app/lib/features/web_feed/data/providers.g.dart @@ -48,4 +48,4 @@ final class FeedDatabaseProvider } } -String _$feedDatabaseHash() => r'c3b20e867da5af6e92d1e8c1efa96b79988837a1'; +String _$feedDatabaseHash() => r'8f24d15d6da7e498bfcd9303698f109028b492fc'; diff --git a/app/lib/main.dart b/app/lib/main.dart index 8b852cc9..4ef64ca1 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -29,6 +29,7 @@ import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:weblibre/core/error_observer.dart'; +import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/app_state.dart'; import 'package:weblibre/core/providers/defaults.dart'; @@ -57,6 +58,7 @@ class _MainWidget extends HookConsumerWidget { .fetchSettings(); await GeckoBrowserService().initialize( + filesystem.relativeProfilePath, kDebugMode ? LogLevel.debug : LogLevel.warn, engineSettings.contentBlocking, engineSettings.addonCollection, @@ -137,6 +139,8 @@ void main() async { return true; }; + await filesystem.init(); + await BackgroundFetch.registerHeadlessTask(backgroundFetch); if (kDebugMode) { diff --git a/app/lib/utils/exit_app.dart b/app/lib/utils/exit_app.dart new file mode 100644 index 00000000..7d2b55f8 --- /dev/null +++ b/app/lib/utils/exit_app.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:weblibre/core/logger.dart'; + +Future exitApp(ProviderContainer container) async { + logger.i('Preparing exit'); + + await SystemNavigator.pop(); + logger.i('SystemNavigator popped'); + + container.dispose(); + logger.i('Provider container disposed'); + + await Future.delayed(const Duration(seconds: 1)).whenComplete(() { + logger.i('Bye !!1'); + exit(0); + }); +} diff --git a/app/lib/utils/filesystem.dart b/app/lib/utils/filesystem.dart new file mode 100644 index 00000000..1d52a9aa --- /dev/null +++ b/app/lib/utils/filesystem.dart @@ -0,0 +1,130 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:uuid/uuid_value.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/domain/entities/profile.dart'; + +const profilesDirName = 'weblibre_profiles'; +const profileDirPrefix = 'profile-'; + +const _startupProfileFileName = 'current_profile'; +const _metadataFile = 'metadata.json'; + +final profileTransformer = + StreamTransformer.fromHandlers( + handleData: (entity, sink) { + if (entity is Directory && + p.basename(entity.path).startsWith(profileDirPrefix)) { + sink.add(entity); + } + }, + ); + +Future> getAvailableProfileDirectories(Directory profilesDir) { + return profilesDir.list().transform(profileTransformer).toList(); +} + +Future readStartupProfile(Directory dir) async { + final file = File(p.join(dir.path, _startupProfileFileName)); + + if (await file.exists()) { + final contents = await file.readAsString(); + try { + return UuidValue.withValidation(contents); + } catch (e, s) { + logger.e('Could not parse profile', error: e, stackTrace: s); + } + } + + return null; +} + +Future writeStartupProfile( + Directory dir, + UuidValue profile, { + bool flush = false, +}) async { + final file = File(p.join(dir.path, _startupProfileFileName)); + await file.writeAsString(profile.uuid, flush: flush); +} + +Future selectStartupProfile(Directory profilesDir) async { + var startupProfile = await readStartupProfile(profilesDir); + final availableProfiles = await getAvailableProfileDirectories(profilesDir); + + if (startupProfile == null) { + final sortedDirs = await sortByAccessTime(availableProfiles); + + for (final dir in sortedDirs) { + try { + startupProfile = extractDirectoryUuid(dir); + await writeStartupProfile(profilesDir, startupProfile); + + break; + } catch (e, s) { + logger.w('Could not parse profile folder', error: e, stackTrace: s); + } + } + } + + return startupProfile; +} + +UuidValue extractDirectoryUuid(Directory dir) => UuidValue.withValidation( + p.basename(dir.path).substring(profileDirPrefix.length), +); + +Directory getProfileDir(Directory profilesDir, UuidValue profileUuid) { + return Directory( + p.join(profilesDir.path, '$profileDirPrefix${profileUuid.uuid}'), + ); +} + +Future readProfileMetadata(Directory profileDir) async { + final file = File(p.join(profileDir.path, _metadataFile)); + if (!await file.exists()) { + return null; + } + + final content = await file.readAsString(); + return Profile.fromJson(jsonDecode(content) as Map); +} + +Future writeProfileMetadata(Directory profileDir, Profile profile) async { + final file = File(p.join(profileDir.path, _metadataFile)); + await file.writeAsString(jsonEncode(profile.toJson()), flush: true); +} + +Future createNewProfile(Directory profilesDir, Profile profile) async { + final profileDir = getProfileDir(profilesDir, profile.uuidValue); + + if (await profileDir.exists()) { + return false; + } + await profileDir.create(); + await writeProfileMetadata(profileDir, profile); + + return true; +} + +Future> sortByAccessTime( + List dirs, { + bool descending = true, +}) async { + final dirsWithStats = await Future.wait( + dirs.map((dir) async { + final stat = await dir.stat(); + return (dir: dir, accessed: stat.accessed); + }), + ); + + dirsWithStats.sort((a, b) { + final comparison = a.accessed.compareTo(b.accessed); + return descending ? -comparison : comparison; + }); + + return dirsWithStats.map((record) => record.dir).toList(); +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7cd3a1d9..d0318896 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -28,7 +28,6 @@ dependencies: flutter_mozilla_components: path: ../packages/flutter_mozilla_components flutter_reorderable_grid_view: ^5.5.2 - flutter_secure_storage: ^10.0.0-beta.4 flutter_slidable: ^4.0.3 flutter_svg: ^2.2.2 go_router: ^17.0.0 @@ -80,7 +79,6 @@ dependencies: timeago: ^3.7.1 tor: path: ../packages/tor - universal_io: ^2.3.1 uri_to_file: git: url: https://github.com/FaFre/uri-to-file.git diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 4d71ea01..89831ed5 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -60,7 +60,7 @@ android { } defaultConfig { - minSdk = 21 + minSdk = 24 } buildFeatures { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt index 404ffeee..dfca0566 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt @@ -16,7 +16,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.CallSuper @@ -30,7 +29,6 @@ import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration import eu.weblibre.flutter_mozilla_components.services.DownloadService import io.flutter.Log -import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.concept.engine.EngineView @@ -242,7 +240,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit shareResourceFeature.set( ShareResourceFeature( - context = requireContext().applicationContext, + context = components.profileApplicationContext, httpClient = components.core.client, store = components.core.store, tabId = sessionId, @@ -253,7 +251,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit downloadsFeature.set( feature = DownloadsFeature( - requireContext().applicationContext, + components.profileApplicationContext, store = components.core.store, useCases = components.useCases.downloadsUseCases, fragmentManager = childFragmentManager, @@ -261,7 +259,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit Logger.debug("Download done. ID#$id $download with status $status") }, downloadManager = FetchDownloadManager( - requireContext().applicationContext, + components.profileApplicationContext, components.core.store, DownloadService::class, notificationsDelegate = components.notificationsDelegate, @@ -449,7 +447,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit } private fun openPopup(webExtensionState: WebExtensionState) { - val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java) + val intent = Intent(components.profileApplicationContext, WebExtensionActionPopupActivity::class.java) intent.putExtra("web_extension_id", webExtensionState.id) intent.putExtra("web_extension_name", webExtensionState.name) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt index 925c15ea..9ee54c29 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt @@ -6,29 +6,19 @@ package eu.weblibre.flutter_mozilla_components -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import androidx.annotation.CallSuper -import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity -import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature -import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature -import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration -import mozilla.components.browser.state.state.WebExtensionState -import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.concept.engine.EngineView -import mozilla.components.feature.tabs.WindowFeature import mozilla.components.support.base.feature.UserInteractionHandler -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import mozilla.components.support.webextensions.WebExtensionPopupObserver /** * Fragment used for browsing the web within the main app. */ class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler { override fun createEngine(components: Components): EngineView { - return components.core.engine.createView(requireContext()).apply { + return components.core.engine.createView(components.profileApplicationContext).apply { selectionActionDelegate = components.selectionAction } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt index 05918c6a..5fe34f14 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt @@ -19,7 +19,6 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents -import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController import mozilla.components.concept.engine.EngineView @@ -32,7 +31,7 @@ import mozilla.components.feature.downloads.FileSizeFormatter import mozilla.components.support.base.android.NotificationsDelegate import mozilla.components.support.base.log.Log -class Components(private val context: Context, +class Components(val profileApplicationContext: Context, val flutterEvents: GeckoStateEvents, val readerViewController: ReaderViewController, val selectionAction: SelectionActionDelegate, @@ -43,24 +42,24 @@ class Components(private val context: Context, private val tabContentEvents: GeckoTabContentEvents, private val extensionEvents: BrowserExtensionEvents ) { - val core by lazy { Core(context, this, flutterEvents, extensionEvents) } + val core by lazy { Core(profileApplicationContext, this, flutterEvents, extensionEvents) } val events by lazy { Events(flutterEvents) } - val useCases by lazy { UseCases(context, core.engine, core.store) } - val services by lazy { Services(context, useCases.tabsUseCases) } + val useCases by lazy { UseCases(profileApplicationContext, core.engine, core.store) } + val services by lazy { Services(profileApplicationContext, useCases.tabsUseCases) } val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) } - val search by lazy { Search(context, core, useCases) } + val search by lazy { Search(profileApplicationContext, core, useCases) } var engineView: EngineView? = null var engineReportedInitialized = false - private val notificationManagerCompat = NotificationManagerCompat.from(context) + private val notificationManagerCompat = NotificationManagerCompat.from(profileApplicationContext) val notificationsDelegate: NotificationsDelegate by lazy { NotificationsDelegate( notificationManagerCompat, ) } - val fileSizeFormatter: FileSizeFormatter by lazy { DefaultFileSizeFormatter(context) } + val fileSizeFormatter: FileSizeFormatter by lazy { DefaultFileSizeFormatter(profileApplicationContext) } val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt index 62b2886a..21d1ab3f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt @@ -37,14 +37,15 @@ object GlobalComponents { get() = _components @DelicateCoroutinesApi - private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) { - newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage) + private fun restoreBrowserState(newComponents: Components) = + GlobalScope.launch(Dispatchers.Main) { + newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage) - newComponents.core.sessionStorage.autoSave(newComponents.core.store) - .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) - .whenGoingToBackground() - .whenSessionsChange() - } + newComponents.core.sessionStorage.autoSave(newComponents.core.store) + .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) + .whenGoingToBackground() + .whenSessionsChange() + } @DelicateCoroutinesApi private fun restoreDownloads(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) { @@ -62,7 +63,7 @@ object GlobalComponents { extensionEvents: BrowserExtensionEvents, logLevel: Log.Priority, contentBlocking: ContentBlocking, - addonCollection: AddonCollection?, + addonCollection: AddonCollection? ) { Logger.debug("Creating new components") @@ -77,7 +78,6 @@ object GlobalComponents { addonEvents, tabContentEvents, extensionEvents, - ) _components = newComponents @@ -103,16 +103,17 @@ object GlobalComponents { WebExtensionSupport.initialize( newComponents.core.engine, newComponents.core.store, - onNewTabOverride = { - _, engineSession, url -> - newComponents.useCases.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession) + onNewTabOverride = { _, engineSession, url -> + newComponents.useCases.tabsUseCases.addTab( + url, + selectTab = true, + engineSession = engineSession + ) }, - onCloseTabOverride = { - _, sessionId -> + onCloseTabOverride = { _, sessionId -> newComponents.useCases.tabsUseCases.removeTab(sessionId) }, - onSelectTabOverride = { - _, sessionId -> + onSelectTabOverride = { _, sessionId -> newComponents.useCases.tabsUseCases.selectTab(sessionId) }, onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ProfileContext.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ProfileContext.kt new file mode 100644 index 00000000..d9eaca7e --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ProfileContext.kt @@ -0,0 +1,127 @@ +package eu.weblibre.flutter_mozilla_components + +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ApplicationInfo +import android.os.Build +import androidx.annotation.RequiresApi +import java.io.File + +class ProfileContext(private val base: Context, private val relativePath: String) : + ContextWrapper(base) { + + private val subfolderRoot = + File(base.filesDir, relativePath) // /data/user/0/com.app/profiles/default + + private var customFilesDir: File = File(subfolderRoot, "files") + private var customNoBackupFilesDir: File = File(subfolderRoot, "no_backup") + private var customObbDir: File = File(subfolderRoot, "obb") + private var customCacheDir: File = File(subfolderRoot, "cache") + private var customCodeCacheDir: File = File(subfolderRoot, "code_cache") + private var customDataDir: File = subfolderRoot + private var customExternalCacheDir: File? = + base.externalCacheDir?.parentFile?.let { File(File(it, relativePath), "cache") } + private var customExternalFilesDir: File? = + base.getExternalFilesDir(null)?.parentFile?.let { File(File(it, relativePath), "files") } + + private val customApplicationInfo: ApplicationInfo by lazy { + val original = base.applicationInfo + ApplicationInfo(original).apply { + dataDir = customDataDir.absolutePath + sourceDir = original.sourceDir + publicSourceDir = original.publicSourceDir + nativeLibraryDir = original.nativeLibraryDir + deviceProtectedDataDir = customDataDir.absolutePath + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + deviceProtectedDataDir = customDataDir.absolutePath + } + } + } + + init { + customFilesDir.mkdirs() + customNoBackupFilesDir.mkdirs() + customObbDir.mkdirs() + customCacheDir.mkdirs() + customCodeCacheDir.mkdirs() + customDataDir.mkdirs() + customExternalCacheDir?.mkdirs() + customExternalFilesDir?.mkdirs() + } + + override fun getApplicationInfo(): ApplicationInfo { + return customApplicationInfo + } + + override fun getFilesDir(): File { + return customFilesDir + } + + override fun getFileStreamPath(name: String): File { + return File(customFilesDir, name) + } + + override fun getNoBackupFilesDir(): File { + return customNoBackupFilesDir + } + + override fun getObbDir(): File { + return customObbDir + } + + override fun getCacheDir(): File { + return customCacheDir + } + + override fun getCodeCacheDir(): File { + return customCodeCacheDir + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun getDataDir(): File { + return customDataDir + } + + override fun getExternalCacheDir(): File? { + return customExternalCacheDir + } + + override fun getExternalFilesDir(type: String?): File? { + return if (type == null) { + customExternalFilesDir + } else { + base.getExternalFilesDir(type)?.parentFile?.let { + File(File(it, relativePath), type) + }?.apply { mkdirs() } + } + } + + override fun getExternalFilesDirs(type: String?): Array { + return base.getExternalFilesDirs(type).map { + File(File(it.parentFile!!, relativePath), type ?: "files").apply { mkdirs() } + }.toTypedArray() + } + + override fun getExternalCacheDirs(): Array { + return base.externalCacheDirs.map { + File(File(it.parentFile!!, relativePath), "cache").apply { mkdirs() } + }.toTypedArray() + } + + override fun getExternalMediaDirs(): Array { + return base.externalMediaDirs.map { + File(File(it.parentFile!!, relativePath), "media").apply { mkdirs() } + }.toTypedArray() + } + + override fun getDir(name: String, mode: Int): File { + return File(customDataDir, name).apply { mkdirs() } + } + + override fun getDatabasePath(name: String): File { + return File(File(customDataDir, "databases"), name).apply { + parentFile?.mkdirs() + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index 53e2a3d4..a3ef6259 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentActivity import eu.weblibre.flutter_mozilla_components.BrowserFragment import eu.weblibre.flutter_mozilla_components.GeckoViewFactory import eu.weblibre.flutter_mozilla_components.GlobalComponents +import eu.weblibre.flutter_mozilla_components.ProfileContext import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection @@ -51,7 +52,6 @@ import mozilla.components.browser.state.action.SystemAction import mozilla.components.feature.addons.logger import mozilla.components.support.base.ext.getStacktraceAsString import mozilla.components.support.base.log.Log -import mozilla.components.support.base.log.sink.AndroidLogSink import mozilla.components.support.base.log.sink.LogSink import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig @@ -71,7 +71,7 @@ class PriorityAwareLogSink( return } - val level = when(priority) { + val level = when (priority) { Log.Priority.DEBUG -> LogLevel.DEBUG Log.Priority.INFO -> LogLevel.INFO Log.Priority.WARN -> LogLevel.WARN @@ -110,7 +110,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { private var isPlatformViewRegistered = false private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding - private lateinit var _flutterEvents : GeckoStateEvents + private lateinit var _flutterEvents: GeckoStateEvents fun attachBinding(flutterPluginBinding: FlutterPluginBinding) { _flutterPluginBinding = flutterPluginBinding @@ -143,15 +143,16 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { } override fun initialize( + profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection? ) { synchronized(this) { - if(!isGeckoInitialized) { + if (!isGeckoInitialized) { val geckoLogging = GeckoLogging(_flutterPluginBinding.binaryMessenger) - val level = when(logLevel) { + val level = when (logLevel) { LogLevel.DEBUG -> Log.Priority.DEBUG LogLevel.INFO -> Log.Priority.INFO LogLevel.WARN -> Log.Priority.WARN @@ -160,7 +161,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { Log.addSink(PriorityAwareLogSink(level, geckoLogging)) - setupGeckoEngine(level, contentBlocking, addonCollection) + setupGeckoEngine(profileFolder, level, contentBlocking, addonCollection) isGeckoInitialized = true } } @@ -177,19 +178,24 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { } private fun setupGeckoEngine( + profileFolder: String, logLevel: Log.Priority, contentBlocking: ContentBlocking, addonCollection: AddonCollection? ) { - val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger) + val profileApplicationContext = ProfileContext(_flutterPluginBinding.applicationContext, profileFolder) - val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents) { actions -> - val processTextAction = "android.intent.action.PROCESS_TEXT" - val withoutProcessText = actions.filter { it != processTextAction }.toTypedArray() - val processTextActions = actions.filter { it == processTextAction }.toTypedArray() + val selectionActionEvents = + GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger) - withoutProcessText + processTextActions - } + val selectionActionDelegate = + DefaultSelectionActionDelegate(selectionActionEvents) { actions -> + val processTextAction = "android.intent.action.PROCESS_TEXT" + val withoutProcessText = actions.filter { it != processTextAction }.toTypedArray() + val processTextActions = actions.filter { it == processTextAction }.toTypedArray() + + withoutProcessText + processTextActions + } val readerViewController = ReaderViewController(_flutterPluginBinding.binaryMessenger) @@ -200,10 +206,13 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger) val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger) - GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents)) + GeckoSuggestionApi.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoSuggestionApiImpl(suggestionEvents) + ) GlobalComponents.setUp( - _flutterPluginBinding.applicationContext, + profileApplicationContext, _flutterEvents, readerViewController, selectionActionDelegate, @@ -215,22 +224,39 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { addonCollection ) - GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl()) - GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(_flutterPluginBinding.applicationContext)) + GeckoEngineSettingsApi.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoEngineSettingsApiImpl() + ) + GeckoAddonsApi.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoAddonsApiImpl(profileApplicationContext) + ) GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl()) GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl()) GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl()) GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl()) GeckoMlApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoMlApiImpl()) GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl()) - GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl()) + GeckoContainerProxyApi.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoContainerProxyApiImpl() + ) GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl()) - GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl( - selectionActionDelegate - )) - GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl()) + GeckoSelectionActionController.setUp( + _flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl( + selectionActionDelegate + ) + ) + GeckoDeleteBrowsingDataController.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoDeleteBrowsingDataControllerImpl() + ) GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl()) - GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl()) + GeckoBrowserExtensionApi.setUp( + _flutterPluginBinding.binaryMessenger, + GeckoBrowserExtensionApiImpl() + ) GeckoHistoryApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoHistoryApiImpl()) GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl()) @@ -239,9 +265,10 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { components.events.readerViewEvents ) - val intent = Intent(_flutterPluginBinding.applicationContext, NotificationActivity::class.java) + val intent = + Intent(profileApplicationContext, NotificationActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - _flutterPluginBinding.applicationContext.startActivity(intent) + profileApplicationContext.startActivity(intent) } private fun showFragmentCallback(): Boolean { @@ -289,7 +316,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { return true } - if(!view.isAttachedToWindow) { + if (!view.isAttachedToWindow) { return true } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 7c2883d5..aee906e0 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -3095,7 +3095,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoBrowserApi { fun getGeckoVersion(): String - fun initialize(logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?) + fun initialize(profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?) fun showNativeFragment(): Boolean fun onTrimMemory(level: Long) @@ -3128,11 +3128,12 @@ interface GeckoBrowserApi { if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List - val logLevelArg = args[0] as LogLevel - val contentBlockingArg = args[1] as ContentBlocking - val addonCollectionArg = args[2] as AddonCollection? + val profileFolderArg = args[0] as String + val logLevelArg = args[1] as LogLevel + val contentBlockingArg = args[2] as ContentBlocking + val addonCollectionArg = args[3] as AddonCollection? val wrapped: List = try { - api.initialize(logLevelArg, contentBlockingArg, addonCollectionArg) + api.initialize(profileFolderArg, logLevelArg, contentBlockingArg, addonCollectionArg) listOf(null) } catch (exception: Throwable) { GeckoPigeonUtils.wrapError(exception) diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart index 98b059cd..5645ebf4 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart @@ -123,10 +123,10 @@ class GeckoAddonService extends GeckoAddonEvents { ); } - void dispose() { - unawaited(_browserExtensionSubject.close()); - unawaited(_pageExtensionSubject.close()); - unawaited(_browserIconSubject.close()); - unawaited(_pageIconSubject.close()); + Future dispose() async { + await _browserExtensionSubject.close(); + await _pageExtensionSubject.close(); + await _browserIconSubject.close(); + await _pageIconSubject.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart index 2b5e0e46..e8232239 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart @@ -18,11 +18,17 @@ class GeckoBrowserService { } Future initialize( + String profileFolder, LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection, ) { - return _api.initialize(logLevel, contentBlocking, addonCollection); + return _api.initialize( + profileFolder, + logLevel, + contentBlocking, + addonCollection, + ); } Future showNativeFragment() { diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart index be18b8b8..8619e8dd 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart @@ -210,23 +210,23 @@ class GeckoEventService extends GeckoStateEvents { ); } - void dispose() { - unawaited(_viewStateSubject.close()); - unawaited(_engineStateSubject.close()); - unawaited(_tabListSubject.close()); - unawaited(_selectedTabSubject.close()); - unawaited(_tabContentSubject.close()); - unawaited(_historySubject.close()); - unawaited(_readerableSubject.close()); - unawaited(_securityInfoSubject.close()); - unawaited(_iconChangeSubject.close()); - unawaited(_iconUpdateSubject.close()); - unawaited(_thumbnailSubject.close()); - unawaited(_findResultsSubject.close()); - unawaited(_longPressSubject.close()); - unawaited(_scrollEventSubject.close()); - unawaited(_tabAddedSubject.close()); - unawaited(_prefUpdateSubject.close()); - unawaited(_siteAssignementSubject.close()); + Future dispose() async { + await _viewStateSubject.close(); + await _engineStateSubject.close(); + await _tabListSubject.close(); + await _selectedTabSubject.close(); + await _tabContentSubject.close(); + await _historySubject.close(); + await _readerableSubject.close(); + await _securityInfoSubject.close(); + await _iconChangeSubject.close(); + await _iconUpdateSubject.close(); + await _thumbnailSubject.close(); + await _findResultsSubject.close(); + await _longPressSubject.close(); + await _scrollEventSubject.close(); + await _tabAddedSubject.close(); + await _prefUpdateSubject.close(); + await _siteAssignementSubject.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart index a2d06da8..ff317967 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart @@ -48,7 +48,7 @@ class GeckoReaderableService extends ReaderViewController { ); } - void dispose() { - unawaited(_appearanceVisibility.close()); + Future dispose() async { + await _appearanceVisibility.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_suggestions.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_suggestions.dart index 64c649f7..7aaa26f2 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_suggestions.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_suggestions.dart @@ -60,7 +60,7 @@ class GeckoSuggestionsService extends GeckoSuggestionEvents { ); } - void dispose() { - unawaited(_suggestionsSubject.close()); + Future dispose() async { + await _suggestionsSubject.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab_content.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab_content.dart index 5b2043ee..0f226701 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab_content.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab_content.dart @@ -32,7 +32,7 @@ class GeckoTabContentService extends GeckoTabContentEvents { _contentSubject.addWhenMoreRecent(timestamp, content.tabId, content); } - void dispose() { - unawaited(_contentSubject.close()); + Future dispose() async { + await _contentSubject.close(); } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 7dbcad08..18342a01 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -3574,14 +3574,14 @@ class GeckoBrowserApi { } } - Future initialize(LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection) async { + Future initialize(String profileFolder, LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([logLevel, contentBlocking, addonCollection]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([profileFolder, logLevel, contentBlocking, addonCollection]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index c831d9dd..be3e1e52 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -877,6 +877,7 @@ class AddonCollection { abstract class GeckoBrowserApi { String getGeckoVersion(); void initialize( + String profileFolder, LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection,