mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-01-25 19:49:33 +00:00
something
This commit is contained in:
parent
dbf4ce1959
commit
a720c977c2
115 changed files with 8819 additions and 1 deletions
48
lib/pages/app_settings.dart
Normal file
48
lib/pages/app_settings.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.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/app_settings_provider.dart';
|
||||
|
||||
class AppSettingsPage extends HookConsumerWidget {
|
||||
const AppSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final registeredServers = ref.watch(audiobookShelfServerProvider);
|
||||
final registeredServersAsList = registeredServers.toList();
|
||||
final availableUsers = ref.watch(authenticatedUserProvider);
|
||||
final serverURIController = useTextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('App Settings'),
|
||||
),
|
||||
body: SettingsList(
|
||||
sections: [
|
||||
SettingsSection(
|
||||
title: const Text('Appearance'),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
initialValue: appSettings.isDarkMode,
|
||||
title: const Text('Dark Mode'),
|
||||
leading: appSettings.isDarkMode
|
||||
? const Icon(Icons.dark_mode)
|
||||
: const Icon(Icons.light_mode),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).toggleDarkMode();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
76
lib/pages/home_page.dart
Normal file
76
lib/pages/home_page.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
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 '../widgets/drawer.dart';
|
||||
import '../widgets/shelves/home_shelf.dart';
|
||||
|
||||
class HomePage extends HookConsumerWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// hooks for the dark mode
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
final views = ref.watch(personalizedViewProvider);
|
||||
final scrollController = useScrollController();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
child: const Text('Whispering Pages'),
|
||||
onTap: () {
|
||||
// scroll to the top of the page
|
||||
scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
// refresh the view
|
||||
ref.invalidate(personalizedViewProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
drawer: const MyDrawer(),
|
||||
body: Container(
|
||||
child: views.when(
|
||||
data: (data) {
|
||||
final shelvesToDisplay = data
|
||||
.where((element) => !element.id.contains('discover'))
|
||||
.map(
|
||||
(shelf) => HomeShelf(
|
||||
title: Text(shelf.label),
|
||||
shelf: shelf,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
// await ref
|
||||
// .read(personalizedViewProvider.notifier)
|
||||
// .forceRefresh();
|
||||
return ref.refresh(personalizedViewProvider);
|
||||
},
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) => shelvesToDisplay[index],
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
itemCount: shelvesToDisplay.length,
|
||||
controller: scrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, stack) {
|
||||
return Text('Error: $error');
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/pages/onboarding/onboarding.dart
Normal file
89
lib/pages/onboarding/onboarding.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import 'package:coast/coast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:whispering_pages/pages/onboarding/server_setup.dart';
|
||||
import 'package:whispering_pages/pages/onboarding/user_login.dart';
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
|
||||
const _serverTag = 'server';
|
||||
|
||||
class OnboardingPage extends StatefulHookConsumerWidget {
|
||||
const OnboardingPage({super.key});
|
||||
|
||||
@override
|
||||
OnboardingPageState createState() => OnboardingPageState();
|
||||
}
|
||||
|
||||
class OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||
final coastController = CoastController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
coastController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
|
||||
final serverUriController = useTextEditingController(
|
||||
text: apiSettings.activeServer?.serverUrl.toString(),
|
||||
);
|
||||
|
||||
bool isUserLoginAvailable() {
|
||||
return apiSettings.activeServer != null;
|
||||
}
|
||||
|
||||
// ignore: invalid_use_of_protected_member
|
||||
if (isUserLoginAvailable()) {
|
||||
try {
|
||||
coastController.animateTo(
|
||||
beach: 1,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final beaches = [
|
||||
Beach(
|
||||
builder: (context) => FirstTimeServerSetupPage(
|
||||
controller: serverUriController,
|
||||
heroServerTag: _serverTag,
|
||||
),
|
||||
),
|
||||
isUserLoginAvailable()
|
||||
? Beach(
|
||||
builder: (context) => FirstTimeUserLoginPage(
|
||||
serverUriController: serverUriController,
|
||||
heroServerTag: _serverTag,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
].nonNulls.toList();
|
||||
const activeStep = 0;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Coast(
|
||||
beaches: beaches,
|
||||
controller: coastController,
|
||||
observers: [
|
||||
CrabController(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
child: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/pages/onboarding/server_setup.dart
Normal file
66
lib/pages/onboarding/server_setup.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:coast/coast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/widgets/add_new_server.dart';
|
||||
|
||||
class FirstTimeServerSetupPage extends HookConsumerWidget {
|
||||
const FirstTimeServerSetupPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.heroServerTag,
|
||||
});
|
||||
final TextEditingController controller;
|
||||
final String heroServerTag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Welcome to Whispering Pages'),
|
||||
Crab(
|
||||
tag: heroServerTag,
|
||||
child: AddNewServer(
|
||||
controller: controller,
|
||||
allowEmpty: true,
|
||||
onPressed: () {
|
||||
var newServer = controller.text.isEmpty
|
||||
? null
|
||||
: model.AudiobookShelfServer(
|
||||
serverUrl: Uri.parse(controller.text),
|
||||
);
|
||||
try {
|
||||
// add the server to the list of servers
|
||||
if (newServer != null) {
|
||||
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
||||
newServer,
|
||||
);
|
||||
}
|
||||
// else remove the server from the list of servers
|
||||
else if (apiSettings.activeServer != null) {
|
||||
ref
|
||||
.read(audiobookShelfServerProvider.notifier)
|
||||
.removeServer(apiSettings.activeServer!);
|
||||
}
|
||||
} on ServerAlreadyExistsException catch (e) {
|
||||
newServer = e.server;
|
||||
} finally {
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeServer: newServer,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/pages/onboarding/user_login.dart
Normal file
94
lib/pages/onboarding/user_login.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:coast/coast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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'
|
||||
show authenticatedUserProvider;
|
||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||
import 'package:whispering_pages/settings/models/authenticated_user.dart';
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
import 'package:whispering_pages/widgets/add_new_server.dart';
|
||||
import 'package:whispering_pages/widgets/user_login.dart';
|
||||
|
||||
|
||||
/// Once the user has selected a server, they can login to it.
|
||||
class FirstTimeUserLoginPage extends HookConsumerWidget {
|
||||
const FirstTimeUserLoginPage({
|
||||
super.key,
|
||||
required this.serverUriController,
|
||||
required this.heroServerTag,
|
||||
});
|
||||
|
||||
final TextEditingController serverUriController;
|
||||
final String heroServerTag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final usernameController = useTextEditingController();
|
||||
final passwordController = useTextEditingController();
|
||||
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
final api = ref
|
||||
.watch(audiobookshelfApiProvider(Uri.https(serverUriController.text)));
|
||||
|
||||
/// Login to the server and save the user
|
||||
void loginAndSave() async {
|
||||
final username = usernameController.text;
|
||||
final password = passwordController.text;
|
||||
final success = await api.login(username: username, password: password);
|
||||
// debugPrint('Login success: $success');
|
||||
if (success != null) {
|
||||
var authenticatedUser = AuthenticatedUser(
|
||||
server: AudiobookShelfServer(
|
||||
serverUrl: Uri.parse(serverUriController.text),
|
||||
),
|
||||
id: success.user.id,
|
||||
password: password,
|
||||
username: username,
|
||||
authToken: api.token!,
|
||||
);
|
||||
// add the user to the list of users
|
||||
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
|
||||
|
||||
// set the active user
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeUser: authenticatedUser,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Login failed'),
|
||||
),
|
||||
);
|
||||
// give focus back to the username field
|
||||
}
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Crab(
|
||||
tag: heroServerTag,
|
||||
child: AddNewServer(
|
||||
controller: serverUriController,
|
||||
onPressed: () {},
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text('Login to server'),
|
||||
const SizedBox(height: 30),
|
||||
UserLogin(
|
||||
usernameController: usernameController,
|
||||
passwordController: passwordController,
|
||||
onPressed: loginAndSave,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
lib/pages/pages.dart
Normal file
2
lib/pages/pages.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'home_page.dart';
|
||||
export 'server_manager.dart';
|
||||
131
lib/pages/server_manager.dart
Normal file
131
lib/pages/server_manager.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
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/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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue