From 5c74abc201a94329e0290fc77476c3c9ceb0f330 Mon Sep 17 00:00:00 2001 From: "Dr.Blank" <64108942+Dr-Blank@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:18:52 +0530 Subject: [PATCH] fix: reuse onboarding components on server manager page --- ...dart => authenticated_users_provider.dart} | 12 +- ...rt => authenticated_users_provider.g.dart} | 22 +- lib/api/server_provider.dart | 4 +- lib/api/server_provider.g.dart | 2 +- lib/features/onboarding/view/user_login.dart | 34 +- .../view/user_login_with_open_id.dart | 2 + .../view/user_login_with_password.dart | 19 +- .../view/user_login_with_token.dart | 17 +- lib/features/you/view/server_manager.dart | 744 ++++++++---------- 9 files changed, 401 insertions(+), 455 deletions(-) rename lib/api/{authenticated_user_provider.dart => authenticated_users_provider.dart} (86%) rename lib/api/{authenticated_user_provider.g.dart => authenticated_users_provider.g.dart} (54%) diff --git a/lib/api/authenticated_user_provider.dart b/lib/api/authenticated_users_provider.dart similarity index 86% rename from lib/api/authenticated_user_provider.dart rename to lib/api/authenticated_users_provider.dart index c4f065c..5e78fab 100644 --- a/lib/api/authenticated_user_provider.dart +++ b/lib/api/authenticated_users_provider.dart @@ -8,15 +8,15 @@ import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/settings/models/authenticated_user.dart' as model; import 'package:vaani/shared/extensions/obfuscation.dart'; -part 'authenticated_user_provider.g.dart'; +part 'authenticated_users_provider.g.dart'; final _box = AvailableHiveBoxes.authenticatedUserBox; -final _logger = Logger('authenticated_user_provider'); +final _logger = Logger('authenticated_users_provider'); /// provides with a set of authenticated users @riverpod -class AuthenticatedUser extends _$AuthenticatedUser { +class AuthenticatedUsers extends _$AuthenticatedUsers { @override Set build() { ref.listenSelf((_, __) { @@ -56,6 +56,7 @@ class AuthenticatedUser extends _$AuthenticatedUser { void addUser(model.AuthenticatedUser user, {bool setActive = false}) { state = state..add(user); + ref.invalidateSelf(); if (setActive) { final apiSettings = ref.read(apiSettingsProvider); ref.read(apiSettingsProvider.notifier).updateState( @@ -82,9 +83,12 @@ class AuthenticatedUser extends _$AuthenticatedUser { // also remove the user from the active user final apiSettings = ref.read(apiSettingsProvider); if (apiSettings.activeUser == user) { + // replace the active user with the first user in the list + // or null if there are no users left + final newActiveUser = state.isNotEmpty ? state.first : null; ref.read(apiSettingsProvider.notifier).updateState( apiSettings.copyWith( - activeUser: null, + activeUser: newActiveUser, ), ); } diff --git a/lib/api/authenticated_user_provider.g.dart b/lib/api/authenticated_users_provider.g.dart similarity index 54% rename from lib/api/authenticated_user_provider.g.dart rename to lib/api/authenticated_users_provider.g.dart index b12f8c6..44a2610 100644 --- a/lib/api/authenticated_user_provider.g.dart +++ b/lib/api/authenticated_users_provider.g.dart @@ -1,28 +1,30 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'authenticated_user_provider.dart'; +part of 'authenticated_users_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$authenticatedUserHash() => r'1983527595207c63a12bc84cf0bf1a3c1d729506'; +String _$authenticatedUsersHash() => + r'5fdd472f62fc3b73ff8417cdce9f02e86c33d00f'; /// provides with a set of authenticated users /// -/// Copied from [AuthenticatedUser]. -@ProviderFor(AuthenticatedUser) -final authenticatedUserProvider = AutoDisposeNotifierProvider>.internal( - AuthenticatedUser.new, - name: r'authenticatedUserProvider', +/// Copied from [AuthenticatedUsers]. +@ProviderFor(AuthenticatedUsers) +final authenticatedUsersProvider = AutoDisposeNotifierProvider< + AuthenticatedUsers, Set>.internal( + AuthenticatedUsers.new, + name: r'authenticatedUsersProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$authenticatedUserHash, + : _$authenticatedUsersHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$AuthenticatedUser = AutoDisposeNotifier>; +typedef _$AuthenticatedUsers + = AutoDisposeNotifier>; // 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 diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart index 6bfefb1..ef1c864 100644 --- a/lib/api/server_provider.dart +++ b/lib/api/server_provider.dart @@ -1,6 +1,6 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/models/audiobookshelf_server.dart' as model; @@ -88,7 +88,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { } // remove the users of this server if (removeUsers) { - ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server); + ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server); } } diff --git a/lib/api/server_provider.g.dart b/lib/api/server_provider.g.dart index 5fc811c..ff2406a 100644 --- a/lib/api/server_provider.g.dart +++ b/lib/api/server_provider.g.dart @@ -7,7 +7,7 @@ part of 'server_provider.dart'; // ************************************************************************** String _$audiobookShelfServerHash() => - r'09e7e37ddc794c45eafbaab7eba82c9dd17faa93'; + r'31a96b431221965cd586aad670a32ca901539e41'; /// provides with a set of servers added by the user /// diff --git a/lib/features/onboarding/view/user_login.dart b/lib/features/onboarding/view/user_login.dart index c5ba4bf..35bb329 100644 --- a/lib/features/onboarding/view/user_login.dart +++ b/lib/features/onboarding/view/user_login.dart @@ -2,35 +2,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/server_provider.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' show AuthMethod; +import 'package:vaani/api/api_provider.dart' show serverStatusProvider; +import 'package:vaani/api/server_provider.dart' + show ServerAlreadyExistsException, audiobookShelfServerProvider; import 'package:vaani/features/onboarding/view/onboarding_single_page.dart' show fadeSlideTransitionBuilder; -import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'; -import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; -import 'package:vaani/features/onboarding/view/user_login_with_token.dart'; -import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; -import 'package:vaani/models/error_response.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart' + show UserLoginWithOpenID; +import 'package:vaani/features/onboarding/view/user_login_with_password.dart' + show UserLoginWithPassword; +import 'package:vaani/features/onboarding/view/user_login_with_token.dart' + show UserLoginWithToken; +import 'package:vaani/hacks/fix_autofill_losing_focus.dart' + show InactiveFocusScopeObserver; +import 'package:vaani/models/error_response.dart' show ErrorResponseHandler; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; import 'package:vaani/settings/models/models.dart' as model; class UserLoginWidget extends HookConsumerWidget { UserLoginWidget({ super.key, required this.server, + this.onSuccess, }); final Uri server; final serverStatusError = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { final serverStatus = ref.watch(serverStatusProvider(server, serverStatusError.storeError)); - final api = ref.watch(audiobookshelfApiProvider(server)); - return serverStatus.when( data: (value) { if (value == null) { @@ -45,6 +51,7 @@ class UserLoginWidget extends HookConsumerWidget { openIDAvailable: value.authMethods?.contains(AuthMethod.openid) ?? false, openIDButtonText: value.authFormData?.authOpenIDButtonText, + onSuccess: onSuccess, ); }, loading: () { @@ -91,6 +98,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget { this.openIDAvailable = false, this.onPressed, this.openIDButtonText, + this.onSuccess, }); final Uri server; @@ -98,6 +106,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget { final bool openIDAvailable; final void Function()? onPressed; final String? openIDButtonText; + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -190,15 +199,18 @@ class UserLoginMultipleAuth extends HookConsumerWidget { AuthMethodChoice.authToken => UserLoginWithToken( server: server, addServer: addServer, + onSuccess: onSuccess, ), AuthMethodChoice.local => UserLoginWithPassword( server: server, addServer: addServer, + onSuccess: onSuccess, ), AuthMethodChoice.openid => UserLoginWithOpenID( server: server, addServer: addServer, openIDButtonText: openIDButtonText, + onSuccess: onSuccess, ), }, ), diff --git a/lib/features/onboarding/view/user_login_with_open_id.dart b/lib/features/onboarding/view/user_login_with_open_id.dart index 71baa42..b3a1d9e 100644 --- a/lib/features/onboarding/view/user_login_with_open_id.dart +++ b/lib/features/onboarding/view/user_login_with_open_id.dart @@ -20,12 +20,14 @@ class UserLoginWithOpenID extends HookConsumerWidget { required this.server, required this.addServer, this.openIDButtonText, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final String? openIDButtonText; final responseErrorHandler = ErrorResponseHandler(name: 'OpenID'); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/features/onboarding/view/user_login_with_password.dart b/lib/features/onboarding/view/user_login_with_password.dart index b730cc5..210da77 100644 --- a/lib/features/onboarding/view/user_login_with_password.dart +++ b/lib/features/onboarding/view/user_login_with_password.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lottie/lottie.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; @@ -18,11 +18,13 @@ class UserLoginWithPassword extends HookConsumerWidget { super.key, required this.server, required this.addServer, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final serverErrorResponse = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -81,13 +83,16 @@ class UserLoginWithPassword extends HookConsumerWidget { username: username, authToken: api.token!, ); - // add the user to the list of users - ref - .read(authenticatedUserProvider.notifier) - .addUser(authenticatedUser, setActive: true); - // redirect to the library page - GoRouter.of(context).goNamed(Routes.home.name); + if (onSuccess != null) { + onSuccess!(authenticatedUser); + } else { + // add the user to the list of users + ref + .read(authenticatedUsersProvider.notifier) + .addUser(authenticatedUser, setActive: true); + context.goNamed(Routes.home.name); + } } return Center( diff --git a/lib/features/onboarding/view/user_login_with_token.dart b/lib/features/onboarding/view/user_login_with_token.dart index a67b96b..7d2fcfb 100644 --- a/lib/features/onboarding/view/user_login_with_token.dart +++ b/lib/features/onboarding/view/user_login_with_token.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/models/models.dart' as model; @@ -14,11 +14,13 @@ class UserLoginWithToken extends HookConsumerWidget { super.key, required this.server, required this.addServer, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final serverErrorResponse = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -65,11 +67,14 @@ class UserLoginWithToken extends HookConsumerWidget { authToken: api.token!, ); - ref - .read(authenticatedUserProvider.notifier) - .addUser(authenticatedUser, setActive: true); - - context.goNamed(Routes.home.name); + if (onSuccess != null) { + onSuccess!(authenticatedUser); + } else { + ref + .read(authenticatedUsersProvider.notifier) + .addUser(authenticatedUser, setActive: true); + context.goNamed(Routes.home.name); + } } return Form( diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart index f44eb77..75e3f43 100644 --- a/lib/features/you/view/server_manager.dart +++ b/lib/features/you/view/server_manager.dart @@ -2,17 +2,22 @@ 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:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; -import 'package:vaani/api/server_provider.dart'; -import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; -import 'package:vaani/main.dart'; -import 'package:vaani/models/error_response.dart'; -import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/api/api_provider.dart' show makeBaseUrl; +import 'package:vaani/api/authenticated_users_provider.dart' + show authenticatedUsersProvider; +import 'package:vaani/api/server_provider.dart' + show ServerAlreadyExistsException, audiobookShelfServerProvider; +import 'package:vaani/features/onboarding/view/user_login.dart' + show UserLoginWidget; +import 'package:vaani/features/player/view/mini_player_bottom_padding.dart' + show MiniPlayerBottomPadding; +import 'package:vaani/main.dart' show appLogger; +import 'package:vaani/router/router.dart' show Routes; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; import 'package:vaani/settings/models/models.dart' as model; -import 'package:vaani/shared/extensions/obfuscation.dart'; -import 'package:vaani/shared/widgets/add_new_server.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet; +import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer; class ServerManagerPage extends HookConsumerWidget { const ServerManagerPage({ @@ -21,15 +26,6 @@ class ServerManagerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final apiSettings = ref.watch(apiSettingsProvider); - final registeredServers = ref.watch(audiobookShelfServerProvider); - final registeredServersAsList = registeredServers.toList(); - final availableUsers = ref.watch(authenticatedUserProvider); - final serverURIController = useTextEditingController(); - final formKey = GlobalKey(); - - appLogger.fine('registered servers: ${registeredServers.obfuscate()}'); - appLogger.fine('available users: ${availableUsers.obfuscate()}'); return Scaffold( appBar: AppBar( title: const Text('Manage Accounts'), @@ -37,241 +33,119 @@ class ServerManagerPage extends HookConsumerWidget { body: Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( - // crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'Registered Servers', - ), - Expanded( - child: ListView.builder( - itemCount: registeredServers.length, - reverse: true, - itemBuilder: (context, index) { - var registeredServer = registeredServersAsList[index]; - return ExpansionTile( - title: Text(registeredServer.serverUrl.toString()), - subtitle: Text( - 'Users: ${availableUsers.where((element) => element.server == registeredServer).length}', - ), - // trailing: _DeleteServerButton( - // registeredServer: registeredServer, - // ), - // children are list of users of this server - children: availableUsers - .where( - (element) => element.server == registeredServer, - ) - .map( - (e) => ListTile( - selected: apiSettings.activeUser == e, - leading: apiSettings.activeUser == e - ? const Icon(Icons.person) - : const Icon(Icons.person_off_outlined), - title: Text(e.username ?? 'Anonymous'), - onTap: apiSettings.activeUser == e - ? null - : () { - ref - .read(apiSettingsProvider.notifier) - .updateState( - apiSettings.copyWith( - activeUser: e, - ), - ); - // pop all routes and go to the home page - // while (context.canPop()) { - // context.pop(); - // } - context.goNamed( - Routes.home.name, - ); - }, - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete User'), - content: const Text( - 'Are you sure you want to delete this user?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - ref - .read( - authenticatedUserProvider - .notifier, - ) - .removeUser(e); - Navigator.of(context).pop(); - }, - child: const Text('Delete'), - ), - ], - ); - }, - ); - }, - ), - ), - ) - .nonNulls - .toList() - - // add buttons of delete server and add user to server at the end - ..addAll([ - ListTile( - leading: const Icon(Icons.person_add), - title: const Text('Add User'), - onTap: () async { - // open a dialog to add a new user with username and password or another method using only auth token - final addedUser = await showDialog( - context: context, - builder: (context) { - return _AddUserDialog( - server: registeredServer, - ); - }, - ); - - // if (addedUser != null) { - // // show a snackbar that the user has been added and ask if change to this user - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: const Text( - // 'User added successfully, do you want to switch to this user?', - // ), - // action: SnackBarAction( - // label: 'Switch', - // onPressed: () { - // // set the active user - // ref - // .read(apiSettingsProvider.notifier) - // .updateState( - // apiSettings.copyWith( - // activeUser: addedUser, - // ), - // ); - - // context.goNamed(Routes.home.name); - // }, - // ), - // ), - // ); - // } - }, - ), - ListTile( - leading: const Icon(Icons.delete), - title: const Text('Delete Server'), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete Server'), - content: const Text( - 'Are you sure you want to delete this server and all its users?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - ref - .read( - audiobookShelfServerProvider - .notifier, - ) - .removeServer( - registeredServer, - removeUsers: true, - ); - Navigator.of(context).pop(); - }, - child: const Text('Delete'), - ), - ], - ); - }, - ); - }, - ), - ]), - ); - }, - ), - ), - const SizedBox(height: 20), - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Add New Server'), - ), - Form( - key: formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: AddNewServer( - controller: serverURIController, - onPressed: () { - if (formKey.currentState!.validate()) { - try { - final newServer = model.AudiobookShelfServer( - serverUrl: makeBaseUrl(serverURIController.text), - ); - ref - .read(audiobookShelfServerProvider.notifier) - .addServer( - newServer, - ); - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeServer: newServer, - ), - ); - serverURIController.clear(); - } on ServerAlreadyExistsException catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toString()), - ), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid URL'), - ), - ); - } - }, - ), - ), - MiniPlayerBottomPadding(), - ], - ), + child: ServerManagerBody(), ), ), ); } } -class _AddUserDialog extends HookConsumerWidget { - const _AddUserDialog({ +class ServerManagerBody extends HookConsumerWidget { + const ServerManagerBody({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final registeredServers = ref.watch(audiobookShelfServerProvider); + final registeredServersAsList = registeredServers.toList(); + final availableUsers = ref.watch(authenticatedUsersProvider); + final apiSettings = ref.watch(apiSettingsProvider); + final serverURIController = useTextEditingController(); + final formKey = GlobalKey(); + + appLogger.fine('registered servers: ${registeredServers.obfuscate()}'); + appLogger.fine('available users: ${availableUsers.obfuscate()}'); + + return Column( + // crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + 'Registered Servers', + ), + Expanded( + child: ListView.builder( + itemCount: registeredServers.length, + reverse: true, + itemBuilder: (context, index) { + var registeredServer = registeredServersAsList[index]; + return ExpansionTile( + title: Text(registeredServer.serverUrl.toString()), + subtitle: Text( + 'Users: ${availableUsers.where((element) => element.server == registeredServer).length}', + ), + // children are list of users of this server + children: availableUsers + .where( + (element) => element.server == registeredServer, + ) + .map( + (e) => AvailableUserTile(user: e), + ) + .nonNulls + .toList() + + // add buttons of delete server and add user to server at the end + ..addAll([ + AddUserTile(server: registeredServer), + DeleteServerTile(server: registeredServer), + ]), + ); + }, + ), + ), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Add New Server'), + ), + Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: AddNewServer( + controller: serverURIController, + onPressed: () { + if (formKey.currentState!.validate()) { + try { + final newServer = model.AudiobookShelfServer( + serverUrl: makeBaseUrl(serverURIController.text), + ); + ref.read(audiobookShelfServerProvider.notifier).addServer( + newServer, + ); + ref.read(apiSettingsProvider.notifier).updateState( + apiSettings.copyWith( + activeServer: newServer, + ), + ); + serverURIController.clear(); + } on ServerAlreadyExistsException catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + ), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invalid URL'), + ), + ); + } + }, + ), + ), + MiniPlayerBottomPadding(), + ], + ); + } +} + +class DeleteServerTile extends HookConsumerWidget { + const DeleteServerTile({ + super.key, required this.server, }); @@ -279,178 +153,220 @@ class _AddUserDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final usernameController = useTextEditingController(); - final passwordController = useTextEditingController(); - final authTokensController = useTextEditingController(); - final isPasswordVisible = useState(false); - final apiSettings = ref.watch(apiSettingsProvider); - final isMethodAuth = useState(false); - final api = ref.watch(audiobookshelfApiProvider(server.serverUrl)); - - final formKey = GlobalKey(); - - final serverErrorResponse = ErrorResponseHandler(); - - /// Login to the server and save the user - Future loginAndSave() async { - model.AuthenticatedUser? authenticatedUser; - if (isMethodAuth.value) { - api.token = authTokensController.text; - final success = await api.misc.authorize( - responseErrorHandler: serverErrorResponse.storeError, - ); - if (success != null) { - authenticatedUser = model.AuthenticatedUser( - server: server, - id: success.user.id, - username: success.user.username, - authToken: api.token!, - ); - } - } else { - final username = usernameController.text; - final password = passwordController.text; - final success = await api.login( - username: username, - password: password, - responseErrorHandler: serverErrorResponse.storeError, - ); - if (success != null) { - authenticatedUser = model.AuthenticatedUser( - server: server, - id: success.user.id, - username: username, - authToken: api.token!, - ); - } - } - // add the user to the list of users - if (authenticatedUser != null) { - ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})', - ), - ), - ); - } - return authenticatedUser; - } - - return AlertDialog( - // title: Text('Add User for ${server.serverUrl}'), - title: Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'Add User for ', - style: Theme.of(context).textTheme.labelLarge, - ), - TextSpan( - text: server.serverUrl.toString(), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, + return ListTile( + leading: const Icon(Icons.delete), + title: const Text('Delete Server'), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Remove Server and Users'), + // Make content scrollable in case of smaller screens/keyboard + content: SingleChildScrollView( + child: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'This will remove the server ', + ), + TextSpan( + text: server.serverUrl.host, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const TextSpan( + text: ' and all its users\' login info from this app.', + ), + ], ), - ), - ], - ), - ), - content: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - alignment: WrapAlignment.center, - spacing: 8.0, - children: [ - ChoiceChip( - label: const Text('Username/Password'), - selected: !isMethodAuth.value, - onSelected: (selected) { - isMethodAuth.value = !selected; - }, ), - ChoiceChip( - label: const Text('Auth Token'), - selected: isMethodAuth.value, - onSelected: (selected) { - isMethodAuth.value = selected; + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref + .read( + audiobookShelfServerProvider.notifier, + ) + .removeServer( + server, + removeUsers: true, + ); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), ), ], - ), - const SizedBox(height: 16), - if (isMethodAuth.value) - TextFormField( - controller: authTokensController, - decoration: const InputDecoration(labelText: 'Auth Token'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an auth token'; - } - return null; - }, - ) - else ...[ - TextFormField( - controller: usernameController, - decoration: const InputDecoration(labelText: 'Username'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username'; - } - return null; - }, - ), - TextFormField( - controller: passwordController, - decoration: InputDecoration( - labelText: 'Password', - suffixIcon: IconButton( - icon: Icon( - isPasswordVisible.value - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - isPasswordVisible.value = !isPasswordVisible.value; - }, - ), - ), - obscureText: !isPasswordVisible.value, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a password'; - } - return null; - }, - ), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + ); }, - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () async { - if (formKey.currentState!.validate()) { - final addedUser = await loginAndSave(); - if (addedUser != null) { - Navigator.of(context).pop(addedUser); - } - } - }, - child: const Text('Add User'), - ), - ], + ); + }, + ); + } +} + +class AddUserTile extends HookConsumerWidget { + const AddUserTile({ + super.key, + required this.server, + }); + + final model.AudiobookShelfServer server; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListTile( + leading: const Icon(Icons.person_add), + title: const Text('Add User'), + onTap: () async { + await showDialog( + context: context, + // barrierDismissible: false, // Optional: prevent closing by tapping outside + builder: (dialogContext) { + // Use a different context name to avoid conflicts + return AlertDialog( + title: Text('Add User to ${server.serverUrl.host}'), + // Make content scrollable in case of smaller screens/keyboard + content: SingleChildScrollView( + child: UserLoginWidget( + server: server.serverUrl, + // Pass the callback to pop the dialog on success + onSuccess: (user) { + // Add the user to the server + ref.read(authenticatedUsersProvider.notifier).addUser(user); + Navigator.of(dialogContext).pop(); // Close the dialog + // Optional: Show a confirmation SnackBar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('User added successfully! Switch?'), + action: SnackBarAction( + label: 'Switch', + onPressed: () { + // Switch to the new user + ref.read(apiSettingsProvider.notifier).updateState( + ref.read(apiSettingsProvider).copyWith( + activeUser: user, + ), + ); + context.goNamed(Routes.home.name); + }, + ), + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); // Close the dialog + }, + child: const Text('Cancel'), + ), + ], + ); + }, + ); + // No need for the SnackBar asking to switch user here anymore. + }, + ); + } +} + +class AvailableUserTile extends HookConsumerWidget { + const AvailableUserTile({ + super.key, + required this.user, + }); + + final model.AuthenticatedUser user; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apiSettings = ref.watch(apiSettingsProvider); + + return ListTile( + selected: apiSettings.activeUser == user, + leading: apiSettings.activeUser == user + ? const Icon(Icons.person) + : const Icon(Icons.person_off_outlined), + title: Text(user.username ?? 'Anonymous'), + onTap: apiSettings.activeUser == user + ? null + : () { + ref.read(apiSettingsProvider.notifier).updateState( + apiSettings.copyWith( + activeUser: user, + ), + ); + // pop all routes and go to the home page + // while (context.canPop()) { + // context.pop(); + // } + context.goNamed( + Routes.home.name, + ); + }, + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Remove User Login'), + content: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'This will remove login details of the user ', + ), + TextSpan( + text: user.username ?? 'Anonymous', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const TextSpan( + text: ' from this app.', + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref + .read( + authenticatedUsersProvider.notifier, + ) + .removeUser(user); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + }, + ), ); } }