diff --git a/.vscode/tasks.json b/.vscode/tasks.json index df7e1c0..e16f58e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -32,37 +32,6 @@ "message": 4 } } - }, - // flutter build apk --release - { - "icon": { "id": "package", "color": "terminal.ansiGreen" }, - "label": "flutter build release APK", - "type": "shell", - "command": "flutter build apk --release", - "group": { - "kind": "none", - "isDefault": false - }, - "detail": "Building APK in release mode", - "presentation": { - "revealProblems": "onProblem", - "reveal": "always", - "panel": "dedicated" - }, - "runOptions": { - "instanceLimit": 1 - }, - "problemMatcher": { - "owner": "dart", - "fileLocation": ["relative", "${workspaceFolder}"], - "pattern": { - "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "message": 4 - } - } } ] } diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart index 748b5e0..2202252 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -32,7 +32,7 @@ AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) { throw ArgumentError.notNull('baseUrl'); } return AudiobookshelfApi( - baseUrl: baseUrl, + baseUrl: makeBaseUrl(baseUrl.toString()), ); } @@ -47,7 +47,7 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) { throw StateError('No active user'); } return AudiobookshelfApi( - baseUrl: Uri.https(user.server.serverUrl.toString()), + baseUrl: makeBaseUrl(user.server.serverUrl.toString()), token: user.authToken, ); } diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart index cc754a7..0cf5307 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$audiobookshelfApiHash() => r'5eb091c6b18c0bf5a0eec079fdb872a84c4f00d9'; +String _$audiobookshelfApiHash() => r'de9cbf9ec0647ac84366e0dc0a175f069d112c0a'; /// Copied from Dart SDK class _SystemHash { @@ -168,7 +168,7 @@ class _AudiobookshelfApiProviderElement Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl; } -String _$authenticatedApiHash() => r'd99ea87b21dfb63b5f6fed8f79e835af42f2296f'; +String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14'; /// get the api instance for the authenticated user /// diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 22eff26..b3d65f3 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -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 libraryItemPlayButtonOnPressed({ ref.read(appSettingsProvider).playerSettings.preferredDefaultVolume, ); } - -Future _launchUrl(Uri url) async { - if (!await launchUrl( - url, - mode: LaunchMode.platformDefault, - webOnlyWindowName: '_blank', - )) { - // throw Exception('Could not launch $url'); - debugPrint('Could not launch $url'); - } -} diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 7a3ed06..dee2b8d 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -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'); } }); } diff --git a/lib/features/onboarding/view/onboarding_single_page.dart b/lib/features/onboarding/view/onboarding_single_page.dart index 9fa465f..c0740f5 100644 --- a/lib/features/onboarding/view/onboarding_single_page.dart +++ b/lib/features/onboarding/view/onboarding_single_page.dart @@ -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 _launchUrl(Uri url) async { - if (!await launchUrl( - url, - mode: LaunchMode.platformDefault, - webOnlyWindowName: '_blank', - )) { - // throw Exception('Could not launch $url'); - debugPrint('Could not launch $url'); - } -} diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart new file mode 100644 index 0000000..18a72f2 --- /dev/null +++ b/lib/features/you/view/server_manager.dart @@ -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(); + + 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(); + + /// 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(); + 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'), + ), + ], + ); + } +} diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart new file mode 100644 index 0000000..c71392c --- /dev/null +++ b/lib/features/you/view/you_page.dart @@ -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'), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2bd877c..3de1136 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:whispering_pages/api/api_provider.dart'; import 'package:whispering_pages/settings/app_settings_provider.dart'; -import '../shared/widgets/drawer.dart'; import '../shared/widgets/shelves/home_shelf.dart'; class HomePage extends HookConsumerWidget { @@ -19,8 +18,12 @@ class HomePage extends HookConsumerWidget { final scrollController = useScrollController(); return Scaffold( appBar: AppBar( + backgroundColor: Colors.transparent, title: GestureDetector( - child: const Text('Vaani'), + child: Text( + 'Vaani', + style: Theme.of(context).textTheme.headlineLarge, + ), onTap: () { // scroll to the top of the page scrollController.animateTo( @@ -33,7 +36,6 @@ class HomePage extends HookConsumerWidget { }, ), ), - drawer: const MyDrawer(), body: Container( child: views.when( data: (data) { @@ -72,7 +74,6 @@ class HomePage extends HookConsumerWidget { } } - class HomePageSkeleton extends StatelessWidget { const HomePageSkeleton({super.key}); diff --git a/lib/pages/server_manager.dart b/lib/pages/server_manager.dart deleted file mode 100644 index 92ec9c5..0000000 --- a/lib/pages/server_manager.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:whispering_pages/api/authenticated_user_provider.dart'; -import 'package:whispering_pages/api/server_provider.dart'; -import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model; -import 'package:whispering_pages/settings/api_settings_provider.dart'; -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(); - - debugPrint('registered servers: $registeredServers'); - debugPrint('available users: $availableUsers'); - return Scaffold( - appBar: AppBar( - title: const Text('Setup Servers'), - ), - 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: IconButton( - icon: const Icon(Icons.delete), - // delete the server from the list of servers - onPressed: () { - ref - .read(audiobookShelfServerProvider.notifier) - .removeServer(registeredServer); - }, - ), - // children are list of users of this server - children: availableUsers - .where( - (element) => element.server == registeredServer, - ) - .map( - (e) => ListTile( - title: Text(e.username ?? 'Anonymous'), - subtitle: Text(e.authToken), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - ref - .read(authenticatedUserProvider.notifier) - .removeUser(e); - }, - ), - ), - ) - .nonNulls - .toList(), - ); - }, - ), - ), - const SizedBox(height: 20), - 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'), - ), - ); - } - }, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/router/constants.dart b/lib/router/constants.dart index fde72b8..deaeba9 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -22,7 +22,7 @@ class Routes { name: 'libraryItem', ); - // settings + // Local settings static const settings = _SimpleRoute( pathName: 'config', name: 'settings', @@ -56,6 +56,18 @@ class Routes { name: 'libraryBrowser', // parentRoute: library, ); + + // you page for the user + static const you = _SimpleRoute( + pathName: 'you', + name: 'you', + ); + + // user management + static const userManagement = _SimpleRoute( + pathName: 'users', + name: 'userManagement', + ); } // a class to store path diff --git a/lib/router/router.dart b/lib/router/router.dart index f06964e..84f0d2c 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -6,6 +6,8 @@ import 'package:whispering_pages/features/explore/view/search_result_page.dart'; import 'package:whispering_pages/features/item_viewer/view/library_item_page.dart'; import 'package:whispering_pages/features/library_browser/view/library_browser_page.dart'; import 'package:whispering_pages/features/onboarding/view/onboarding_single_page.dart'; +import 'package:whispering_pages/features/you/view/server_manager.dart'; +import 'package:whispering_pages/features/you/view/you_page.dart'; import 'package:whispering_pages/pages/home_page.dart'; import 'package:whispering_pages/settings/view/app_settings_page.dart'; import 'package:whispering_pages/settings/view/auto_sleep_timer_settings_page.dart'; @@ -139,9 +141,14 @@ class MyAppRouter { ), ], ), - // settings page + // you page StatefulShellBranch( routes: [ + GoRoute( + path: Routes.you.path, + name: Routes.you.name, + pageBuilder: defaultPageBuilder(const YouPage()), + ), GoRoute( path: Routes.settings.path, name: Routes.settings.name, @@ -157,6 +164,12 @@ class MyAppRouter { const AutoSleepTimerSettingsPage(), ), ), + GoRoute( + path: Routes.userManagement.path, + name: Routes.userManagement.name, + // builder: (context, state) => const UserManagementPage(), + pageBuilder: defaultPageBuilder(const ServerManagerPage()), + ), ], ), ], diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index d5484be..210f1ae 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -198,9 +198,9 @@ const _navigationItems = [ activeIcon: Icons.search, ), _NavigationItem( - name: 'Settings', - icon: Icons.settings_outlined, - activeIcon: Icons.settings, + name: 'You', + icon: Icons.account_circle_outlined, + activeIcon: Icons.account_circle, ), ]; diff --git a/lib/shared/utils.dart b/lib/shared/utils.dart new file mode 100644 index 0000000..6fce1a0 --- /dev/null +++ b/lib/shared/utils.dart @@ -0,0 +1,14 @@ + +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future handleLaunchUrl(Uri url) async { + if (!await launchUrl( + url, + mode: LaunchMode.platformDefault, + webOnlyWindowName: '_blank', + )) { + // throw Exception('Could not launch $url'); + debugPrint('Could not launch $url'); + } +} diff --git a/lib/shared/widgets/add_new_server.dart b/lib/shared/widgets/add_new_server.dart index 652a2d6..97e9298 100644 --- a/lib/shared/widgets/add_new_server.dart +++ b/lib/shared/widgets/add_new_server.dart @@ -43,7 +43,7 @@ class AddNewServer extends HookConsumerWidget { decoration: InputDecoration( labelText: 'Server URI', labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), ), border: const OutlineInputBorder(), prefixText: 'https://', @@ -56,7 +56,7 @@ class AddNewServer extends HookConsumerWidget { child: newServerURI.text.isEmpty ? Icon( Icons.cloud_outlined, - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, ) : isServerAlive.when( data: (value) { @@ -90,7 +90,7 @@ class AddNewServer extends HookConsumerWidget { icon: const Icon(Icons.add), tooltip: 'Add new server', color: Theme.of(context).colorScheme.inversePrimary, - focusColor: Theme.of(context).colorScheme.onBackground, + focusColor: Theme.of(context).colorScheme.onSurface, // should be enabled when onPressed: !readOnly && diff --git a/lib/shared/widgets/drawer.dart b/lib/shared/widgets/drawer.dart index d7c7a25..e38cb40 100644 --- a/lib/shared/widgets/drawer.dart +++ b/lib/shared/widgets/drawer.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:whispering_pages/pages/server_manager.dart'; +import 'package:whispering_pages/features/you/view/server_manager.dart'; import 'package:whispering_pages/router/router.dart'; diff --git a/lib/shared/widgets/shelves/home_shelf.dart b/lib/shared/widgets/shelves/home_shelf.dart index c2d3a7f..869f741 100644 --- a/lib/shared/widgets/shelves/home_shelf.dart +++ b/lib/shared/widgets/shelves/home_shelf.dart @@ -63,7 +63,7 @@ class SimpleHomeShelf extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.only(left: 8.0, bottom: 8.0), - child: Text(title, style: Theme.of(context).textTheme.titleLarge), + child: Text(title, style: Theme.of(context).textTheme.titleMedium), ), // fix the height of the shelf as a percentage of the screen height SizedBox(