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

31
.vscode/tasks.json vendored
View file

@ -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
}
}
}
]
}

View file

@ -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,
);
}

View file

@ -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
///

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'),
],
),
),
),
],
),
);
}
}

View file

@ -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});

View file

@ -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'),
),
);
}
},
),
),
],
),
),
),
);
}
}

View file

@ -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

View file

@ -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()),
),
],
),
],

View file

@ -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
View 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');
}
}

View file

@ -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 &&

View file

@ -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';

View file

@ -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(