you page and switch users

This commit is contained in:
Dr-Blank 2024-08-23 03:44:44 -04:00
parent 3e405b795d
commit 3f496c57c4
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
17 changed files with 659 additions and 211 deletions

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:url_launcher/url_launcher.dart';
import 'package:whispering_pages/api/library_item_provider.dart';
import 'package:whispering_pages/constants/hero_tag_conventions.dart';
import 'package:whispering_pages/features/downloads/providers/download_manager.dart'
@ -19,6 +18,7 @@ import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/shared/extensions/model_conversions.dart';
import 'package:whispering_pages/shared/utils.dart';
class LibraryItemActions extends HookConsumerWidget {
LibraryItemActions({
@ -78,7 +78,7 @@ class LibraryItemActions extends HookConsumerWidget {
currentServerUrl =
Uri.https(currentServerUrl.toString());
}
_launchUrl(
handleLaunchUrl(
Uri.parse(
currentServerUrl.toString() +
(Routes.libraryItem.pathParamName != null
@ -462,14 +462,3 @@ Future<void> libraryItemPlayButtonOnPressed({
ref.read(appSettingsProvider).playerSettings.preferredDefaultVolume,
);
}
Future<void> _launchUrl(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.platformDefault,
webOnlyWindowName: '_blank',
)) {
// throw Exception('Could not launch $url');
debugPrint('Could not launch $url');
}
}

View file

@ -8,6 +8,7 @@ import 'package:whispering_pages/api/image_provider.dart';
import 'package:whispering_pages/constants/hero_tag_conventions.dart';
import 'package:whispering_pages/features/item_viewer/view/library_item_page.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/main.dart';
import 'package:whispering_pages/router/models/library_item_extras.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/shared/extensions/duration_format.dart';
@ -373,7 +374,7 @@ class _BookCover extends HookConsumerWidget {
: themeData,
);
} catch (e) {
debugPrint('Error changing theme: $e');
appLogger.shout('Error changing theme: $e');
}
});
}

View file

@ -3,7 +3,6 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/api/server_provider.dart';
@ -11,6 +10,7 @@ import 'package:whispering_pages/features/onboarding/view/user_login.dart';
import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/models/models.dart' as model;
import 'package:whispering_pages/shared/utils.dart';
import 'package:whispering_pages/shared/widgets/add_new_server.dart';
class OnboardingSinglePage extends HookConsumerWidget {
@ -24,8 +24,9 @@ class OnboardingSinglePage extends HookConsumerWidget {
final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString() ?? '',
);
final api = ref
.watch(audiobookshelfApiProvider(Uri.https(serverUriController.text)));
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
final api = ref.watch(audiobookshelfApiProvider(audiobookshelfUri));
final isUserLoginAvailable = useState(apiSettings.activeServer != null);
@ -183,8 +184,8 @@ class RedirectToABS extends StatelessWidget {
autofocus: false,
isSemanticButton: false,
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
padding: MaterialStateProperty.all(
elevation: WidgetStateProperty.all(0),
padding: WidgetStateProperty.all(
const EdgeInsets.all(0),
),
),
@ -192,7 +193,7 @@ class RedirectToABS extends StatelessWidget {
// open the github page
// ignore: avoid_print
print('Opening the github page');
await _launchUrl(
await handleLaunchUrl(
Uri.parse(
'https://www.audiobookshelf.org',
),
@ -206,14 +207,3 @@ class RedirectToABS extends StatelessWidget {
);
}
}
Future<void> _launchUrl(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.platformDefault,
webOnlyWindowName: '_blank',
)) {
// throw Exception('Could not launch $url');
debugPrint('Could not launch $url');
}
}

View file

@ -0,0 +1,426 @@
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:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/models/models.dart' as model;
import 'package:whispering_pages/shared/widgets/add_new_server.dart';
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'),
),
],
);
}
}

View file

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/shared/utils.dart';
import 'package:whispering_pages/shared/widgets/not_implemented.dart';
class YouPage extends HookConsumerWidget {
const YouPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final me = ref.watch(meProvider);
return me.when(
data: (data) {
return _YouPage(userData: data);
},
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}
class _YouPage extends HookConsumerWidget {
const _YouPage({
super.key,
required this.userData,
});
final User userData;
@override
Widget build(BuildContext context, WidgetRef ref) {
final api = ref.watch(authenticatedApiProvider);
return Scaffold(
appBar: AppBar(
// title: const Text('You'),
backgroundColor: Colors.transparent,
actions: [
// IconButton(
// icon: const Icon(Icons.edit),
// onPressed: () {
// // Handle edit profile
// },
// ),
// settings button
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
context.pushNamed(Routes.settings.name);
},
),
],
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 40,
// backgroundImage: NetworkImage(userData.avatarUrl),
// first letter of the username
child: Text(
userData.username[0].toUpperCase(),
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 16),
Text(
userData.username,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
children: [
ActionChip(
avatar: const Icon(Icons.switch_account_outlined),
label: const Text('Switch Account'),
onPressed: () {
context.pushNamed(Routes.userManagement.name);
},
),
// ActionChip(
// avatar: const Icon(Icons.logout),
// label: const Text('Logout'),
// onPressed: () {
// // Handle logout
// },
// ),
// ActionChip(
// avatar: const Icon(Icons.privacy_tip),
// label: const Text('Incognito Mode'),
// onPressed: () {
// // Handle incognito mode
// },
// ),
],
),
const SizedBox(height: 16),
ListTile(
leading: const Icon(Icons.playlist_play),
title: const Text('My Playlists'),
onTap: () {
// Handle navigation to playlists
},
),
ListTile(
leading: const Icon(Icons.help),
title: const Text('Help'),
onTap: () {
// Handle navigation to help website
showNotImplementedToast(context);
},
),
ListTile(
leading: const Icon(Icons.info),
title: const Text('About'),
onTap: () {
// Handle navigation to about
showNotImplementedToast(context);
},
),
ListTile(
leading: const Icon(Icons.web),
title: const Text('Web Version'),
onTap: () {
handleLaunchUrl(
// get url from api and launch it
api.baseUrl,
);
},
),
// const SizedBox(height: 16),
// const Text('App Version: 1.0.0'),
// const Text('Server Version: 1.0.0'),
// const Text('Author: Your Name'),
],
),
),
),
],
),
);
}
}