mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 11:09:28 +00:00
fix: keyboard not showing when adding new user (#79)
* feat: add fadeSlideTransitionBuilder for smoother transitions in user login * fix: reuse onboarding components on server manager page * fix: gaining focus rebuilt the widget using memoized fixes this issue
This commit is contained in:
parent
c8767b4e1e
commit
25be7fda03
10 changed files with 447 additions and 493 deletions
|
|
@ -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/settings/models/authenticated_user.dart' as model;
|
||||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'authenticated_user_provider.g.dart';
|
part 'authenticated_users_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
||||||
|
|
||||||
final _logger = Logger('authenticated_user_provider');
|
final _logger = Logger('authenticated_users_provider');
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
/// provides with a set of authenticated users
|
||||||
@riverpod
|
@riverpod
|
||||||
class AuthenticatedUser extends _$AuthenticatedUser {
|
class AuthenticatedUsers extends _$AuthenticatedUsers {
|
||||||
@override
|
@override
|
||||||
Set<model.AuthenticatedUser> build() {
|
Set<model.AuthenticatedUser> build() {
|
||||||
ref.listenSelf((_, __) {
|
ref.listenSelf((_, __) {
|
||||||
|
|
@ -56,6 +56,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
|
|
||||||
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
||||||
state = state..add(user);
|
state = state..add(user);
|
||||||
|
ref.invalidateSelf();
|
||||||
if (setActive) {
|
if (setActive) {
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
|
@ -82,9 +83,12 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
// also remove the user from the active user
|
// also remove the user from the active user
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
if (apiSettings.activeUser == user) {
|
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(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
apiSettings.copyWith(
|
apiSettings.copyWith(
|
||||||
activeUser: null,
|
activeUser: newActiveUser,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'authenticated_user_provider.dart';
|
part of 'authenticated_users_provider.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$authenticatedUserHash() => r'1983527595207c63a12bc84cf0bf1a3c1d729506';
|
String _$authenticatedUsersHash() =>
|
||||||
|
r'5fdd472f62fc3b73ff8417cdce9f02e86c33d00f';
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
/// provides with a set of authenticated users
|
||||||
///
|
///
|
||||||
/// Copied from [AuthenticatedUser].
|
/// Copied from [AuthenticatedUsers].
|
||||||
@ProviderFor(AuthenticatedUser)
|
@ProviderFor(AuthenticatedUsers)
|
||||||
final authenticatedUserProvider = AutoDisposeNotifierProvider<AuthenticatedUser,
|
final authenticatedUsersProvider = AutoDisposeNotifierProvider<
|
||||||
Set<model.AuthenticatedUser>>.internal(
|
AuthenticatedUsers, Set<model.AuthenticatedUser>>.internal(
|
||||||
AuthenticatedUser.new,
|
AuthenticatedUsers.new,
|
||||||
name: r'authenticatedUserProvider',
|
name: r'authenticatedUsersProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$authenticatedUserHash,
|
: _$authenticatedUsersHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$AuthenticatedUser = AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
|
typedef _$AuthenticatedUsers
|
||||||
|
= AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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,6 +1,6 @@
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.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/db/storage.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
|
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
|
||||||
|
|
@ -88,7 +88,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
}
|
}
|
||||||
// remove the users of this server
|
// remove the users of this server
|
||||||
if (removeUsers) {
|
if (removeUsers) {
|
||||||
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
|
ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'server_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$audiobookShelfServerHash() =>
|
String _$audiobookShelfServerHash() =>
|
||||||
r'09e7e37ddc794c45eafbaab7eba82c9dd17faa93';
|
r'31a96b431221965cd586aad670a32ca901539e41';
|
||||||
|
|
||||||
/// provides with a set of servers added by the user
|
/// provides with a set of servers added by the user
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -39,22 +39,7 @@ class OnboardingSinglePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OnboardingBody extends HookConsumerWidget {
|
Widget fadeSlideTransitionBuilder(
|
||||||
const OnboardingBody({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
|
||||||
final serverUriController = useTextEditingController(
|
|
||||||
text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
|
|
||||||
);
|
|
||||||
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
|
|
||||||
|
|
||||||
final canUserLogin = useState(apiSettings.activeServer != null);
|
|
||||||
|
|
||||||
fadeSlideTransitionBuilder(
|
|
||||||
Widget child,
|
Widget child,
|
||||||
Animation<double> animation,
|
Animation<double> animation,
|
||||||
) {
|
) {
|
||||||
|
|
@ -70,6 +55,21 @@ class OnboardingBody extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OnboardingBody extends HookConsumerWidget {
|
||||||
|
const OnboardingBody({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final serverUriController = useTextEditingController(
|
||||||
|
text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
|
||||||
|
);
|
||||||
|
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
|
||||||
|
|
||||||
|
final canUserLogin = useState(apiSettings.activeServer != null);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,42 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart' show AuthMethod;
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart' show serverStatusProvider;
|
||||||
import 'package:vaani/api/server_provider.dart';
|
import 'package:vaani/api/server_provider.dart'
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart';
|
show ServerAlreadyExistsException, audiobookShelfServerProvider;
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_password.dart';
|
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_token.dart';
|
show fadeSlideTransitionBuilder;
|
||||||
import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
|
import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'
|
||||||
import 'package:vaani/models/error_response.dart';
|
show UserLoginWithOpenID;
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
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;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
|
||||||
class UserLoginWidget extends HookConsumerWidget {
|
class UserLoginWidget extends HookConsumerWidget {
|
||||||
UserLoginWidget({
|
UserLoginWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final serverStatusError = ErrorResponseHandler();
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serverStatusError = useMemoized(() => ErrorResponseHandler(), []);
|
||||||
final serverStatus =
|
final serverStatus =
|
||||||
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
|
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
|
||||||
|
|
||||||
final api = ref.watch(audiobookshelfApiProvider(server));
|
|
||||||
|
|
||||||
return serverStatus.when(
|
return serverStatus.when(
|
||||||
data: (value) {
|
data: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
|
@ -42,6 +51,7 @@ class UserLoginWidget extends HookConsumerWidget {
|
||||||
openIDAvailable:
|
openIDAvailable:
|
||||||
value.authMethods?.contains(AuthMethod.openid) ?? false,
|
value.authMethods?.contains(AuthMethod.openid) ?? false,
|
||||||
openIDButtonText: value.authFormData?.authOpenIDButtonText,
|
openIDButtonText: value.authFormData?.authOpenIDButtonText,
|
||||||
|
onSuccess: onSuccess,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () {
|
loading: () {
|
||||||
|
|
@ -88,6 +98,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
this.openIDAvailable = false,
|
this.openIDAvailable = false,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.openIDButtonText,
|
this.openIDButtonText,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
|
|
@ -95,6 +106,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
final bool openIDAvailable;
|
final bool openIDAvailable;
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final String? openIDButtonText;
|
final String? openIDButtonText;
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -104,8 +116,6 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
|
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
|
||||||
|
|
||||||
model.AudiobookShelfServer addServer() {
|
model.AudiobookShelfServer addServer() {
|
||||||
var newServer = model.AudiobookShelfServer(
|
var newServer = model.AudiobookShelfServer(
|
||||||
serverUrl: server,
|
serverUrl: server,
|
||||||
|
|
@ -119,7 +129,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
newServer = e.server;
|
newServer = e.server;
|
||||||
} finally {
|
} finally {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
apiSettings.copyWith(
|
ref.read(apiSettingsProvider).copyWith(
|
||||||
activeServer: newServer,
|
activeServer: newServer,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -172,27 +182,37 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
].animate(interval: 100.ms).fadeIn(
|
||||||
|
duration: 150.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: 200.ms,
|
||||||
|
transitionBuilder: fadeSlideTransitionBuilder,
|
||||||
child: switch (methodChoice.value) {
|
child: switch (methodChoice.value) {
|
||||||
AuthMethodChoice.authToken => UserLoginWithToken(
|
AuthMethodChoice.authToken => UserLoginWithToken(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
AuthMethodChoice.local => UserLoginWithPassword(
|
AuthMethodChoice.local => UserLoginWithPassword(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
AuthMethodChoice.openid => UserLoginWithOpenID(
|
AuthMethodChoice.openid => UserLoginWithOpenID(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
openIDButtonText: openIDButtonText,
|
openIDButtonText: openIDButtonText,
|
||||||
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
this.openIDButtonText,
|
this.openIDButtonText,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final String? openIDButtonText;
|
final String? openIDButtonText;
|
||||||
final responseErrorHandler = ErrorResponseHandler(name: 'OpenID');
|
final responseErrorHandler = ErrorResponseHandler(name: 'OpenID');
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/api/api_provider.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/hacks/fix_autofill_losing_focus.dart';
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
|
|
@ -18,11 +18,13 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
final serverErrorResponse = ErrorResponseHandler();
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -81,13 +83,16 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
username: username,
|
username: username,
|
||||||
authToken: api.token!,
|
authToken: api.token!,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess!(authenticatedUser);
|
||||||
|
} else {
|
||||||
// add the user to the list of users
|
// add the user to the list of users
|
||||||
ref
|
ref
|
||||||
.read(authenticatedUserProvider.notifier)
|
.read(authenticatedUsersProvider.notifier)
|
||||||
.addUser(authenticatedUser, setActive: true);
|
.addUser(authenticatedUser, setActive: true);
|
||||||
|
context.goNamed(Routes.home.name);
|
||||||
// redirect to the library page
|
}
|
||||||
GoRouter.of(context).goNamed(Routes.home.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/api/api_provider.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/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
|
@ -14,11 +14,13 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
final serverErrorResponse = ErrorResponseHandler();
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -65,12 +67,15 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
authToken: api.token!,
|
authToken: api.token!,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onSuccess != null) {
|
||||||
|
onSuccess!(authenticatedUser);
|
||||||
|
} else {
|
||||||
ref
|
ref
|
||||||
.read(authenticatedUserProvider.notifier)
|
.read(authenticatedUsersProvider.notifier)
|
||||||
.addUser(authenticatedUser, setActive: true);
|
.addUser(authenticatedUser, setActive: true);
|
||||||
|
|
||||||
context.goNamed(Routes.home.name);
|
context.goNamed(Routes.home.name);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,22 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart' show makeBaseUrl;
|
||||||
import 'package:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_users_provider.dart'
|
||||||
import 'package:vaani/api/server_provider.dart';
|
show authenticatedUsersProvider;
|
||||||
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
|
import 'package:vaani/api/server_provider.dart'
|
||||||
import 'package:vaani/main.dart';
|
show ServerAlreadyExistsException, audiobookShelfServerProvider;
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/features/onboarding/view/user_login.dart'
|
||||||
import 'package:vaani/router/router.dart';
|
show UserLoginWidget;
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
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/settings/models/models.dart' as model;
|
||||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet;
|
||||||
import 'package:vaani/shared/widgets/add_new_server.dart';
|
import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer;
|
||||||
|
|
||||||
class ServerManagerPage extends HookConsumerWidget {
|
class ServerManagerPage extends HookConsumerWidget {
|
||||||
const ServerManagerPage({
|
const ServerManagerPage({
|
||||||
|
|
@ -21,15 +26,6 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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<FormState>();
|
|
||||||
|
|
||||||
appLogger.fine('registered servers: ${registeredServers.obfuscate()}');
|
|
||||||
appLogger.fine('available users: ${availableUsers.obfuscate()}');
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Manage Accounts'),
|
title: const Text('Manage Accounts'),
|
||||||
|
|
@ -37,7 +33,31 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: ServerManagerBody(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FormState>();
|
||||||
|
|
||||||
|
appLogger.fine('registered servers: ${registeredServers.obfuscate()}');
|
||||||
|
appLogger.fine('available users: ${availableUsers.obfuscate()}');
|
||||||
|
|
||||||
|
return Column(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -55,163 +75,21 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
||||||
),
|
),
|
||||||
// trailing: _DeleteServerButton(
|
|
||||||
// registeredServer: registeredServer,
|
|
||||||
// ),
|
|
||||||
// children are list of users of this server
|
// children are list of users of this server
|
||||||
children: availableUsers
|
children: availableUsers
|
||||||
.where(
|
.where(
|
||||||
(element) => element.server == registeredServer,
|
(element) => element.server == registeredServer,
|
||||||
)
|
)
|
||||||
.map(
|
.map<Widget>(
|
||||||
(e) => ListTile(
|
(e) => AvailableUserTile(user: e),
|
||||||
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
|
.nonNulls
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
// add buttons of delete server and add user to server at the end
|
// add buttons of delete server and add user to server at the end
|
||||||
..addAll([
|
..addAll([
|
||||||
ListTile(
|
AddUserTile(server: registeredServer),
|
||||||
leading: const Icon(Icons.person_add),
|
DeleteServerTile(server: registeredServer),
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -233,9 +111,7 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
final newServer = model.AudiobookShelfServer(
|
final newServer = model.AudiobookShelfServer(
|
||||||
serverUrl: makeBaseUrl(serverURIController.text),
|
serverUrl: makeBaseUrl(serverURIController.text),
|
||||||
);
|
);
|
||||||
ref
|
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
||||||
.read(audiobookShelfServerProvider.notifier)
|
|
||||||
.addServer(
|
|
||||||
newServer,
|
newServer,
|
||||||
);
|
);
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
|
@ -263,15 +139,13 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
MiniPlayerBottomPadding(),
|
MiniPlayerBottomPadding(),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddUserDialog extends HookConsumerWidget {
|
class DeleteServerTile extends HookConsumerWidget {
|
||||||
const _AddUserDialog({
|
const DeleteServerTile({
|
||||||
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -279,156 +153,193 @@ class _AddUserDialog extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController();
|
return ListTile(
|
||||||
final passwordController = useTextEditingController();
|
leading: const Icon(Icons.delete),
|
||||||
final authTokensController = useTextEditingController();
|
title: const Text('Delete Server'),
|
||||||
final isPasswordVisible = useState(false);
|
onTap: () {
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
showDialog(
|
||||||
final isMethodAuth = useState(false);
|
context: context,
|
||||||
final api = ref.watch(audiobookshelfApiProvider(server.serverUrl));
|
builder: (context) {
|
||||||
|
|
||||||
final formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
|
||||||
|
|
||||||
/// Login to the server and save the user
|
|
||||||
Future<model.AuthenticatedUser?> 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(
|
return AlertDialog(
|
||||||
// title: Text('Add User for ${server.serverUrl}'),
|
title: const Text('Remove Server and Users'),
|
||||||
title: Text.rich(
|
// Make content scrollable in case of smaller screens/keyboard
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
const TextSpan(
|
||||||
text: 'Add User for ',
|
text: 'This will remove the server ',
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: server.serverUrl.toString(),
|
text: server.serverUrl.host,
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
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;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isPasswordVisible.value = !isPasswordVisible.value;
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
),
|
TextButton(
|
||||||
obscureText: !isPasswordVisible.value,
|
onPressed: () {
|
||||||
validator: (value) {
|
ref
|
||||||
if (value == null || value.isEmpty) {
|
.read(
|
||||||
return 'Please enter a password';
|
audiobookShelfServerProvider.notifier,
|
||||||
}
|
)
|
||||||
return null;
|
.removeServer(
|
||||||
|
server,
|
||||||
|
removeUsers: true,
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
child: const Text('Delete'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -439,18 +350,23 @@ class _AddUserDialog extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
if (formKey.currentState!.validate()) {
|
ref
|
||||||
final addedUser = await loginAndSave();
|
.read(
|
||||||
if (addedUser != null) {
|
authenticatedUsersProvider.notifier,
|
||||||
Navigator.of(context).pop(addedUser);
|
)
|
||||||
}
|
.removeUser(user);
|
||||||
}
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Add User'),
|
child: const Text('Delete'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue