Vaani/lib/features/you/view/server_manager.dart

427 lines
18 KiB
Dart
Raw Normal View History

2024-08-23 03:44:44 -04:00
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';
2024-08-23 04:21:46 -04:00
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/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/models.dart' as model;
import 'package:vaani/shared/widgets/add_new_server.dart';
2024-08-23 03:44:44 -04:00
class ServerManagerPage extends HookConsumerWidget {
const ServerManagerPage({
super.key,
});
@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<FormState>();
debugPrint('registered servers: $registeredServers');
debugPrint('available users: $availableUsers');
return Scaffold(
appBar: AppBar(
title: const Text('Manage Accounts'),
),
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: Uri.parse(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'),
),
);
}
},
),
),
],
),
),
),
);
}
}
class _AddUserDialog extends HookConsumerWidget {
const _AddUserDialog({
super.key,
required this.server,
});
final model.AudiobookShelfServer server;
@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<FormState>();
/// 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();
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);
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(
const SnackBar(
content: Text('Login failed. Please check your credentials.'),
),
);
}
return authenticatedUser;
}
return AlertDialog(
title: const Text('Add User'),
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,
),
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'),
),
],
);
}
}