intermediate

This commit is contained in:
Fabian Freund
2025-01-23 14:03:55 +01:00
parent bd45ed64ee
commit 78c17a70c5
122 changed files with 8730 additions and 1662 deletions
+9 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:lensai/features/user/data/database/database.dart';
@@ -13,7 +15,7 @@ part 'providers.g.dart';
@Riverpod(keepAlive: true)
UserDatabase userDatabase(Ref ref) {
return UserDatabase(
final db = UserDatabase(
LazyDatabase(() async {
// put the database file, called db.sqlite here, into the documents folder
// for your app.
@@ -35,4 +37,10 @@ UserDatabase userDatabase(Ref ref) {
return NativeDatabase.createInBackground(file);
}),
);
ref.onDispose(() {
unawaited(db.close());
});
return db;
}
+1 -1
View File
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$userDatabaseHash() => r'a84f951e1673e34b6857e1618cd7d80497c6f3a1';
String _$userDatabaseHash() => r'717de293750f5da91dc7da3f964bd07f5dd61e10';
/// See also [userDatabase].
@ProviderFor(userDatabase)
@@ -0,0 +1,49 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:lensai/features/user/data/providers.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
const _authKey = 'pb_auth';
@Riverpod()
Stream<double> iconCacheSizeMegabytes(Ref ref) {
final repository = ref.watch(userDatabaseProvider);
return repository.cacheDao.iconCacheSize().watchSingle();
}
@Riverpod()
Future<String?> _storedAuthData(Ref ref) {
const secureStorage = FlutterSecureStorage();
return secureStorage.read(key: _authKey);
}
@Riverpod(keepAlive: true)
AsyncAuthStore authStore(Ref ref) {
const secureStorage = FlutterSecureStorage();
final intial =
ref.watch(_storedAuthDataProvider.select((value) => value.valueOrNull));
return AsyncAuthStore(
initial: intial,
save: (data) async {
await secureStorage.write(key: _authKey, value: data);
},
);
}
@Riverpod()
Stream<AuthStoreEvent> authState(Ref ref) {
final authStore = ref.watch(authStoreProvider);
return authStore.onChange;
}
@Riverpod(keepAlive: true)
PocketBase pocketBase(Ref ref) {
final authStore = ref.watch(authStoreProvider);
return PocketBase('http://192.168.2.104:8090', authStore: authStore);
}
@@ -0,0 +1,94 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$iconCacheSizeMegabytesHash() =>
r'43162fce3ce8b04a5a55ed52021b274afbde2861';
/// See also [iconCacheSizeMegabytes].
@ProviderFor(iconCacheSizeMegabytes)
final iconCacheSizeMegabytesProvider =
AutoDisposeStreamProvider<double>.internal(
iconCacheSizeMegabytes,
name: r'iconCacheSizeMegabytesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$iconCacheSizeMegabytesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef<double>;
String _$storedAuthDataHash() => r'5f7e3ef6233a2036f7ce3728131901a46b1e548e';
/// See also [_storedAuthData].
@ProviderFor(_storedAuthData)
final _storedAuthDataProvider = AutoDisposeFutureProvider<String?>.internal(
_storedAuthData,
name: r'_storedAuthDataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$storedAuthDataHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef _StoredAuthDataRef = AutoDisposeFutureProviderRef<String?>;
String _$authStoreHash() => r'766c001bbb8d7dc4e148eff3816c414d78e77f26';
/// See also [authStore].
@ProviderFor(authStore)
final authStoreProvider = Provider<AsyncAuthStore>.internal(
authStore,
name: r'authStoreProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$authStoreHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AuthStoreRef = ProviderRef<AsyncAuthStore>;
String _$authStateHash() => r'ffa411ae0abc5c7bb3e8a93e783bd8398ce7f666';
/// See also [authState].
@ProviderFor(authState)
final authStateProvider = AutoDisposeStreamProvider<AuthStoreEvent>.internal(
authState,
name: r'authStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$authStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AuthStateRef = AutoDisposeStreamProviderRef<AuthStoreEvent>;
String _$pocketBaseHash() => r'9e1dc8057e300bc7ca697da9f68ec61e70fdbaab';
/// See also [pocketBase].
@ProviderFor(pocketBase)
final pocketBaseProvider = Provider<PocketBase>.internal(
pocketBase,
name: r'pocketBaseProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$pocketBaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PocketBaseRef = ProviderRef<PocketBase>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -1,11 +0,0 @@
import 'package:lensai/features/user/data/providers.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'cache.g.dart';
@Riverpod()
Stream<double> iconCacheSizeMegabytes(Ref ref) {
final repository = ref.watch(userDatabaseProvider);
return repository.cacheDao.iconCacheSize().watchSingle();
}
@@ -0,0 +1,52 @@
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/features/user/extensions/client_exception.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth.g.dart';
class AuthException implements Exception {
final String message;
AuthException(this.message);
@override
String toString() {
return message;
}
}
@Riverpod()
class AuthRepository extends _$AuthRepository {
late PocketBase _pb;
Future<RecordAuth> authWithPassword(String user, String password) async {
try {
return await _pb.collection('users').authWithPassword(user, password);
} on ClientException catch (e) {
throw AuthException(e.errorMessage);
}
}
Future<RecordModel> createUserWithPassword(
String email,
String password,
) async {
try {
return await _pb.collection('users').create(
body: {
"email": email,
"password": password,
"passwordConfirm": password,
},
);
} on ClientException catch (e) {
throw AuthException(e.errorMessage);
}
}
@override
void build() {
_pb = ref.watch(pocketBaseProvider);
}
}
@@ -1,29 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cache.dart';
part of 'auth.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$iconCacheSizeMegabytesHash() =>
r'43162fce3ce8b04a5a55ed52021b274afbde2861';
String _$authRepositoryHash() => r'90ea6f082ef968831763c7f7ee31863f1fe329fe';
/// See also [iconCacheSizeMegabytes].
@ProviderFor(iconCacheSizeMegabytes)
final iconCacheSizeMegabytesProvider =
AutoDisposeStreamProvider<double>.internal(
iconCacheSizeMegabytes,
name: r'iconCacheSizeMegabytesProvider',
/// See also [AuthRepository].
@ProviderFor(AuthRepository)
final authRepositoryProvider =
AutoDisposeNotifierProvider<AuthRepository, void>.internal(
AuthRepository.new,
name: r'authRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$iconCacheSizeMegabytesHash,
: _$authRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef<double>;
typedef _$AuthRepository = AutoDisposeNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,24 @@
import 'package:lensai/extensions/string.dart';
import 'package:pocketbase/pocketbase.dart';
extension ClientExceptionExtension on ClientException {
String get errorMessage {
var message = "${response["message"] as String? ?? ""}\n\n";
final data = response["data"] as Map<String, dynamic>;
try {
data.forEach((key, value) {
final detailedMessage = (value as Map<String, dynamic>)["message"];
if (detailedMessage != null) {
message += '${key.toCapitalized()}: $detailedMessage\n';
}
});
} catch (_) {}
try {
if (originalError != null) {
message += ' $originalError';
}
} catch (_) {}
return message.trim();
}
}
@@ -0,0 +1,33 @@
import 'package:lensai/features/user/domain/repositories/auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'controllers.g.dart';
@Riverpod()
class AuthController extends _$AuthController {
@override
FutureOr<void> build() {}
Future<void> authWithPassword(String user, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref
.read(authRepositoryProvider.notifier)
.authWithPassword(user, password),
);
}
Future<void> registerWithPassword(String user, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() {
final repo = ref.read(authRepositoryProvider.notifier);
return repo
.createUserWithPassword(user, password)
.then((_) => repo.authWithPassword(user, password));
});
}
void clearState() {
state = const AsyncValue.data(null);
}
}
@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'controllers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authControllerHash() => r'5b56651948683d669f29946fe02bdb004f436afb';
/// See also [AuthController].
@ProviderFor(AuthController)
final authControllerProvider =
AutoDisposeAsyncNotifierProvider<AuthController, void>.internal(
AuthController.new,
name: r'authControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AuthController = AutoDisposeAsyncNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,203 @@
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:lensai/features/user/domain/providers.dart';
import 'package:lensai/features/user/presentation/controllers/controllers.dart';
enum _AuthType {
login,
signup,
}
class UserAuthScreen extends HookConsumerWidget {
const UserAuthScreen();
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final authType = useState(_AuthType.login);
final userTextController = useTextEditingController();
final passwordTextController = useTextEditingController();
final confirmPasswordTextController = useTextEditingController();
final authState = ref.watch(authControllerProvider);
ref.listen(
authStateProvider.select((value) => value.valueOrNull),
(previous, next) {
if (next?.token.isNotEmpty ?? false) {
context.pop(true);
}
},
);
return Dialog(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: userTextController,
decoration: const InputDecoration(
label: Text('Email'),
icon: Icon(Icons.account_circle),
),
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Email must be provided';
}
return null;
},
keyboardType: TextInputType.emailAddress,
),
const SizedBox(
height: 8.0,
),
HookBuilder(
builder: (context) {
final obscure = useState(true);
return Column(
children: [
TextFormField(
controller: passwordTextController,
decoration: InputDecoration(
label: const Text('Password'),
icon: const Icon(Icons.lock),
suffixIcon: IconButton(
onPressed: () {
obscure.value = !obscure.value;
},
icon: Icon(
obscure.value
? Icons.visibility
: Icons.visibility_off,
),
),
),
obscureText: obscure.value,
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Password must be provided';
}
return null;
},
),
if (authType.value == _AuthType.signup)
TextFormField(
controller: confirmPasswordTextController,
decoration: InputDecoration(
label: const Text('Confirm Password'),
icon: const Icon(Icons.lock),
suffixIcon: IconButton(
onPressed: () {
obscure.value = !obscure.value;
},
icon: Icon(
obscure.value
? Icons.visibility
: Icons.visibility_off,
),
),
),
obscureText: obscure.value,
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Password must be provided';
}
if (value != passwordTextController.text) {
return 'Password not matching';
}
return null;
},
),
],
);
},
),
const SizedBox(
height: 16,
),
if (authState.hasError && !authState.isLoading)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
authState.error.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
if (!authState.isLoading)
switch (authType.value) {
_AuthType.login => FilledButton(
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
final controller =
ref.read(authControllerProvider.notifier);
await controller.authWithPassword(
userTextController.text,
passwordTextController.text,
);
}
},
child: const Text('Login'),
),
_AuthType.signup => FilledButton(
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
final controller =
ref.read(authControllerProvider.notifier);
await controller.registerWithPassword(
userTextController.text,
passwordTextController.text,
);
}
},
child: const Text('Signup'),
)
}
else
const CircularProgressIndicator(),
if (!authState.isLoading)
switch (authType.value) {
_AuthType.login => TextButton(
onPressed: () {
final controller =
ref.read(authControllerProvider.notifier);
controller.clearState();
authType.value = _AuthType.signup;
},
child: const Text('Signup'),
),
_AuthType.signup => TextButton(
onPressed: () {
final controller =
ref.read(authControllerProvider.notifier);
controller.clearState();
authType.value = _AuthType.login;
},
child: const Text('Login'),
),
},
],
),
),
),
);
}
}