initial multi user feature
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,4 +41,4 @@ final class RouterProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$routerHash() => r'ab1d1e2ea27dd41fe78d7430bfeba87051d88d36';
|
||||
String _$routerHash() => r'cbaa7e982114942303574f9573f4a75b79955583';
|
||||
|
||||
@@ -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>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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._();
|
||||
|
||||
+36
-23
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -60,7 +60,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
||||
+4
-6
@@ -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
|
||||
|
||||
+1
-11
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+7
-8
@@ -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() }
|
||||
|
||||
|
||||
+17
-16
@@ -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,
|
||||
|
||||
+127
@@ -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<File> {
|
||||
return base.getExternalFilesDirs(type).map {
|
||||
File(File(it.parentFile!!, relativePath), type ?: "files").apply { mkdirs() }
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun getExternalCacheDirs(): Array<File> {
|
||||
return base.externalCacheDirs.map {
|
||||
File(File(it.parentFile!!, relativePath), "cache").apply { mkdirs() }
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun getExternalMediaDirs(): Array<File> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-26
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -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<Any?>
|
||||
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<Any?> = try {
|
||||
api.initialize(logLevelArg, contentBlockingArg, addonCollectionArg)
|
||||
api.initialize(profileFolderArg, logLevelArg, contentBlockingArg, addonCollectionArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
|
||||
@@ -123,10 +123,10 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(_browserExtensionSubject.close());
|
||||
unawaited(_pageExtensionSubject.close());
|
||||
unawaited(_browserIconSubject.close());
|
||||
unawaited(_pageIconSubject.close());
|
||||
Future<void> dispose() async {
|
||||
await _browserExtensionSubject.close();
|
||||
await _pageExtensionSubject.close();
|
||||
await _browserIconSubject.close();
|
||||
await _pageIconSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ class GeckoBrowserService {
|
||||
}
|
||||
|
||||
Future<void> initialize(
|
||||
String profileFolder,
|
||||
LogLevel logLevel,
|
||||
ContentBlocking contentBlocking,
|
||||
AddonCollection? addonCollection,
|
||||
) {
|
||||
return _api.initialize(logLevel, contentBlocking, addonCollection);
|
||||
return _api.initialize(
|
||||
profileFolder,
|
||||
logLevel,
|
||||
contentBlocking,
|
||||
addonCollection,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> showNativeFragment() {
|
||||
|
||||
@@ -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<void> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class GeckoReaderableService extends ReaderViewController {
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(_appearanceVisibility.close());
|
||||
Future<void> dispose() async {
|
||||
await _appearanceVisibility.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class GeckoSuggestionsService extends GeckoSuggestionEvents {
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(_suggestionsSubject.close());
|
||||
Future<void> dispose() async {
|
||||
await _suggestionsSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class GeckoTabContentService extends GeckoTabContentEvents {
|
||||
_contentSubject.addWhenMoreRecent(timestamp, content.tabId, content);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(_contentSubject.close());
|
||||
Future<void> dispose() async {
|
||||
await _contentSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3574,14 +3574,14 @@ class GeckoBrowserApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initialize(LogLevel logLevel, ContentBlocking contentBlocking, AddonCollection? addonCollection) async {
|
||||
Future<void> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[logLevel, contentBlocking, addonCollection]);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[profileFolder, logLevel, contentBlocking, addonCollection]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
|
||||
@@ -877,6 +877,7 @@ class AddonCollection {
|
||||
abstract class GeckoBrowserApi {
|
||||
String getGeckoVersion();
|
||||
void initialize(
|
||||
String profileFolder,
|
||||
LogLevel logLevel,
|
||||
ContentBlocking contentBlocking,
|
||||
AddonCollection? addonCollection,
|
||||
|
||||
Reference in New Issue
Block a user