mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-07 03:29:29 +00:00
you page and switch users
This commit is contained in:
parent
3e405b795d
commit
3f496c57c4
17 changed files with 659 additions and 211 deletions
31
.vscode/tasks.json
vendored
31
.vscode/tasks.json
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
426
lib/features/you/view/server_manager.dart
Normal file
426
lib/features/you/view/server_manager.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/features/you/view/you_page.dart
Normal file
164
lib/features/you/view/you_page.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<FormState>();
|
||||
|
||||
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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: <RouteBase>[
|
||||
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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
|||
14
lib/shared/utils.dart
Normal file
14
lib/shared/utils.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
Future<void> handleLaunchUrl(Uri url) async {
|
||||
if (!await launchUrl(
|
||||
url,
|
||||
mode: LaunchMode.platformDefault,
|
||||
webOnlyWindowName: '_blank',
|
||||
)) {
|
||||
// throw Exception('Could not launch $url');
|
||||
debugPrint('Could not launch $url');
|
||||
}
|
||||
}
|
||||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue