initial multi user feature
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user