initial multi user feature

This commit is contained in:
Fabian Freund
2025-11-27 07:51:16 +01:00
parent 6d3c2757c3
commit 00d0599dde
49 changed files with 1341 additions and 258 deletions
+175
View File
@@ -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<List<Directory>> getAvailableProfileDirectories() {
return profilesDir.list().transform(fs.profileTransformer).toList();
}
Future<Profile?> readProfileMetadata(Directory profileDir) {
return fs.readProfileMetadata(profileDir);
}
Directory getProfileDir(UuidValue uuid) {
return fs.getProfileDir(profilesDir, uuid);
}
Future<bool> createNewProfile(Profile profile) {
return fs.createNewProfile(profilesDir, profile);
}
Future<void> updateProfileMetadata(Profile profile) {
return fs.writeProfileMetadata(getProfileDir(profile.uuidValue), profile);
}
Future<void> setStartupProfile(UuidValue profile) {
return fs.writeStartupProfile(profilesDir, profile, flush: true);
}
Future<void> _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<void> _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<void> _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<void> 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<void> _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'));
}
}
+2 -1
View File
@@ -17,9 +17,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart: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) {
+1 -1
View File
@@ -49,6 +49,6 @@ Future<GoRouter> router(Ref ref) async {
return GoRouter(
debugLogDiagnostics: true,
routes: $appRoutes,
initialLocation: initialLocation ?? BrowserRoute().location,
initialLocation: initialLocation ?? const BrowserRoute().location,
);
}
+1 -1
View File
@@ -41,4 +41,4 @@ final class RouterProvider
}
}
String _$routerHash() => r'ab1d1e2ea27dd41fe78d7430bfeba87051d88d36';
String _$routerHash() => r'cbaa7e982114942303574f9573f4a75b79955583';
+66 -1
View File
@@ -64,11 +64,28 @@ part of 'routes.dart';
name: 'OpenSharedContentRoute',
path: 'open_content',
),
TypedGoRoute<SelectProfileRoute>(
name: 'SelectProfileRoute',
path: 'profile',
),
TypedGoRoute<ProfileListRoute>(
name: 'ProfileListRoute',
path: 'profiles',
routes: [
TypedGoRoute<EditProfileRoute>(name: 'ProfileEditScreen', path: 'edit'),
TypedGoRoute<CreateProfileRoute>(
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<void> buildPage(BuildContext context, GoRouterState state) {
return DialogPage(builder: (_) => const TabViewScreen());
}
}
class SelectProfileRoute extends GoRouteData with $SelectProfileRoute {
const SelectProfileRoute();
@override
Page<void> 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<String, dynamic>),
);
}
}
+4
View File
@@ -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';
+117 -7
View File
@@ -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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin $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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin $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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
mixin $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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
T? _$convertMapValue<T>(
String key,
Map<String, String> map,
+31
View File
@@ -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<Object?> get hashParameters => [id, name];
factory Profile.fromJson(Map<String, dynamic> json) =>
_$ProfileFromJson(json);
Map<String, dynamic> toJson() => _$ProfileToJson(this);
}
+68
View File
@@ -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<String, dynamic> json) =>
Profile(id: json['id'] as String, name: json['name'] as String);
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
};
+5 -14
View File
@@ -17,14 +17,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart: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);
}),
);
+1 -1
View File
@@ -48,4 +48,4 @@ final class BangDatabaseProvider
}
}
String _$bangDatabaseHash() => r'e46ec1582f1a21337a302bef1b7963c2e11468d9';
String _$bangDatabaseHash() => r'86fed6bcc4a1e8a0621869c2886b1b80d353f14f';
@@ -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;
@@ -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._();
@@ -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'),
@@ -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;
@@ -54,7 +54,7 @@ final class ReaderableServiceProvider
}
}
String _$readerableServiceHash() => r'03182c8afc41f5184322a3206473cbfa96df5819';
String _$readerableServiceHash() => r'0c432ede496d85ed6a7d028af346b129d97f7502';
@ProviderFor(appearanceButtonVisibility)
const appearanceButtonVisibilityProvider =
@@ -17,14 +17,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart: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) {
@@ -48,4 +48,4 @@ final class TabDatabaseProvider
}
}
String _$tabDatabaseHash() => r'422bd4789296dc271fabbd5906f2e2ab16bccaa3';
String _$tabDatabaseHash() => r'337dbcf30bd57dcee409aec0aa93f1f6f2783368';
+5 -14
View File
@@ -17,15 +17,16 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart: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);
}),
);
+1 -1
View File
@@ -48,7 +48,7 @@ final class UserDatabaseProvider
}
}
String _$userDatabaseHash() => r'b925780435806f0241d7ddb635e301e9fc8baf6e';
String _$userDatabaseHash() => r'8cc40197f9dbb85ccd8263984218753d3690954b';
@ProviderFor(riverpodDatabaseStorage)
const riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._();
@@ -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);
},
),
],
);
}
}
@@ -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<FormState>());
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<bool?>(
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: <Widget>[
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();
}
}
},
),
),
],
),
),
),
);
}
}
@@ -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),
),
);
}
}
+9 -9
View File
@@ -18,26 +18,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<String?> _storedAuthData(Ref ref) {
const secureStorage = FlutterSecureStorage();
return secureStorage.read(key: _authKey);
}
@Riverpod()
Stream<double> iconCacheSizeMegabytes(Ref ref) {
final repository = ref.watch(userDatabaseProvider);
@@ -73,3 +67,9 @@ Future<Result<FingerprintOverrides>> fingerprintOverrideSettings(
return overrides;
}
@Riverpod(keepAlive: true)
Future<Profile> selectedProfile(Ref ref) async {
final profiles = await ref.watch(profileRepositoryProvider.future);
return profiles.firstWhere((p) => p.uuidValue == filesystem.selectedProfile);
}
+33 -33
View File
@@ -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<AsyncValue<String?>, String?, FutureOr<String?>>
with $FutureModifier<String?>, $FutureProvider<String?> {
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<String?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<String?> 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<AsyncValue<Profile>, Profile, FutureOr<Profile>>
with $FutureModifier<Profile>, $FutureProvider<Profile> {
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<Profile> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Profile> create(Ref ref) {
return selectedProfile(ref);
}
}
String _$selectedProfileHash() => r'c703cad8f30abb4f5f42db0119756ee6791ac477';
@@ -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<List<Profile>> _readProfiles() {
return filesystem.getAvailableProfileDirectories().then((dirs) async {
final profiles = await Future.wait(
dirs.map(filesystem.readProfileMetadata),
);
return profiles.nonNulls.toList();
});
}
Future<void> switchProfile(String id) async {
await filesystem.setStartupProfile(UuidValue.withValidation(id));
}
Future<Profile> 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<void> updateProfileMetadata(Profile profile) async {
await filesystem.updateProfileMetadata(profile);
state = await AsyncValue.guard(_readProfiles);
}
Future<bool> 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<List<Profile>> build() {
return _readProfiles();
}
}
@@ -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<ProfileRepository, List<Profile>> {
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<List<Profile>> {
FutureOr<List<Profile>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<List<Profile>>, List<Profile>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Profile>>, List<Profile>>,
AsyncValue<List<Profile>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
+5 -14
View File
@@ -17,14 +17,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart: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);
}),
);
@@ -48,4 +48,4 @@ final class FeedDatabaseProvider
}
}
String _$feedDatabaseHash() => r'c3b20e867da5af6e92d1e8c1efa96b79988837a1';
String _$feedDatabaseHash() => r'8f24d15d6da7e498bfcd9303698f109028b492fc';
+4
View File
@@ -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) {
+20
View File
@@ -0,0 +1,20 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.dart';
import 'package:weblibre/core/logger.dart';
Future<void> 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);
});
}
+130
View File
@@ -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<FileSystemEntity, Directory>.fromHandlers(
handleData: (entity, sink) {
if (entity is Directory &&
p.basename(entity.path).startsWith(profileDirPrefix)) {
sink.add(entity);
}
},
);
Future<List<Directory>> getAvailableProfileDirectories(Directory profilesDir) {
return profilesDir.list().transform(profileTransformer).toList();
}
Future<UuidValue?> 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<void> 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<UuidValue?> 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<Profile?> 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<String, dynamic>);
}
Future<void> writeProfileMetadata(Directory profileDir, Profile profile) async {
final file = File(p.join(profileDir.path, _metadataFile));
await file.writeAsString(jsonEncode(profile.toJson()), flush: true);
}
Future<bool> 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<List<Directory>> sortByAccessTime(
List<Directory> 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();
}
-2
View File
@@ -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