feat: api token login

This commit is contained in:
Dr-Blank 2024-09-06 15:10:00 -04:00
parent 880960c745
commit 682631fb8e
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
17 changed files with 993 additions and 254 deletions

View file

@ -2,6 +2,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
@ -10,6 +11,12 @@ import 'package:vaani/settings/api_settings_provider.dart';
part 'api_provider.g.dart'; part 'api_provider.g.dart';
// TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718
typedef ResponseErrorHandler = void Function(
Response response, [
Object? error,
]);
final _logger = Logger('api_provider'); final _logger = Logger('api_provider');
Uri makeBaseUrl(String address) { Uri makeBaseUrl(String address) {
@ -52,25 +59,33 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
/// ping the server to check if it is reachable /// ping the server to check if it is reachable
@riverpod @riverpod
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async { FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
// return (await ref.watch(audiobookshelfApiProvider).server.ping()) ?? false;
// if address not starts with http or https, add https
// !remove this line
// return true;
if (address.isEmpty) { if (address.isEmpty) {
return false; return false;
} }
if (!address.startsWith('http') && !address.startsWith('https')) {
address = 'https://$address';
}
// check url is valid try {
if (!Uri.parse(address).isAbsolute) { return await AudiobookshelfApi(baseUrl: makeBaseUrl(address))
.server
.ping() ??
false;
} catch (e) {
return false; return false;
} }
return await AudiobookshelfApi(baseUrl: Uri.parse(address)).server.ping() ?? }
false;
/// fetch status of server
@riverpod
FutureOr<ServerStatusResponse?> serverStatus(
ServerStatusRef ref,
Uri baseUrl, [
ResponseErrorHandler? responseErrorHandler,
]) async {
_logger.fine('fetching server status: $baseUrl');
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
final res =
await api.server.status(responseErrorHandler: responseErrorHandler);
_logger.fine('server status: $res');
return res;
} }
/// fetch the personalized view /// fetch the personalized view
@ -97,13 +112,18 @@ class PersonalizedView extends _$PersonalizedView {
) ?? ) ??
await apiResponseCacheManager.getFileFromCache(key); await apiResponseCacheManager.getFileFromCache(key);
if (cachedRes != null) { if (cachedRes != null) {
final resJson = jsonDecode(await cachedRes.file.readAsString()) as List; _logger.fine('reading from cache: $cachedRes for key: $key');
final res = [ try {
for (final item in resJson) final resJson = jsonDecode(await cachedRes.file.readAsString()) as List;
Shelf.fromJson(item as Map<String, dynamic>), final res = [
]; for (final item in resJson)
_logger.fine('reading from cache: $cachedRes'); Shelf.fromJson(item as Map<String, dynamic>),
yield res; ];
_logger.fine('successfully read from cache key: $key');
yield res;
} catch (e) {
_logger.warning('error reading from cache: $e\n$cachedRes');
}
} }
// ! exagerated delay // ! exagerated delay
@ -112,14 +132,20 @@ class PersonalizedView extends _$PersonalizedView {
.getPersonalized(libraryId: apiSettings.activeLibraryId!); .getPersonalized(libraryId: apiSettings.activeLibraryId!);
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}'); // debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
// save to cache // save to cache
final newFile = await apiResponseCacheManager.putFile( if (res != null) {
key, final newFile = await apiResponseCacheManager.putFile(
utf8.encode(jsonEncode(res)), key,
fileExtension: 'json', utf8.encode(jsonEncode(res)),
key: key, fileExtension: 'json',
); key: key,
_logger.fine('writing to cache: $newFile'); );
yield res!; _logger.fine('writing to cache: $newFile');
yield res;
} else {
_logger.warning('failed to fetch personalized view');
yield [];
}
} }
// method to force refresh the view and ignore the cache // method to force refresh the view and ignore the cache

View file

@ -187,7 +187,7 @@ final authenticatedApiProvider = Provider<AudiobookshelfApi>.internal(
); );
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>; typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>;
String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a'; String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6';
/// ping the server to check if it is reachable /// ping the server to check if it is reachable
/// ///
@ -327,6 +327,166 @@ class _IsServerAliveProviderElement
String get address => (origin as IsServerAliveProvider).address; String get address => (origin as IsServerAliveProvider).address;
} }
String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99';
/// fetch status of server
///
/// Copied from [serverStatus].
@ProviderFor(serverStatus)
const serverStatusProvider = ServerStatusFamily();
/// fetch status of server
///
/// Copied from [serverStatus].
class ServerStatusFamily extends Family<AsyncValue<ServerStatusResponse?>> {
/// fetch status of server
///
/// Copied from [serverStatus].
const ServerStatusFamily();
/// fetch status of server
///
/// Copied from [serverStatus].
ServerStatusProvider call(
Uri baseUrl, [
void Function(Response, [Object?])? responseErrorHandler,
]) {
return ServerStatusProvider(
baseUrl,
responseErrorHandler,
);
}
@override
ServerStatusProvider getProviderOverride(
covariant ServerStatusProvider provider,
) {
return call(
provider.baseUrl,
provider.responseErrorHandler,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'serverStatusProvider';
}
/// fetch status of server
///
/// Copied from [serverStatus].
class ServerStatusProvider
extends AutoDisposeFutureProvider<ServerStatusResponse?> {
/// fetch status of server
///
/// Copied from [serverStatus].
ServerStatusProvider(
Uri baseUrl, [
void Function(Response, [Object?])? responseErrorHandler,
]) : this._internal(
(ref) => serverStatus(
ref as ServerStatusRef,
baseUrl,
responseErrorHandler,
),
from: serverStatusProvider,
name: r'serverStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$serverStatusHash,
dependencies: ServerStatusFamily._dependencies,
allTransitiveDependencies:
ServerStatusFamily._allTransitiveDependencies,
baseUrl: baseUrl,
responseErrorHandler: responseErrorHandler,
);
ServerStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
required this.responseErrorHandler,
}) : super.internal();
final Uri baseUrl;
final void Function(Response, [Object?])? responseErrorHandler;
@override
Override overrideWith(
FutureOr<ServerStatusResponse?> Function(ServerStatusRef provider) create,
) {
return ProviderOverride(
origin: this,
override: ServerStatusProvider._internal(
(ref) => create(ref as ServerStatusRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
responseErrorHandler: responseErrorHandler,
),
);
}
@override
AutoDisposeFutureProviderElement<ServerStatusResponse?> createElement() {
return _ServerStatusProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ServerStatusProvider &&
other.baseUrl == baseUrl &&
other.responseErrorHandler == responseErrorHandler;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, baseUrl.hashCode);
hash = _SystemHash.combine(hash, responseErrorHandler.hashCode);
return _SystemHash.finish(hash);
}
}
mixin ServerStatusRef on AutoDisposeFutureProviderRef<ServerStatusResponse?> {
/// The parameter `baseUrl` of this provider.
Uri get baseUrl;
/// The parameter `responseErrorHandler` of this provider.
void Function(Response, [Object?])? get responseErrorHandler;
}
class _ServerStatusProviderElement
extends AutoDisposeFutureProviderElement<ServerStatusResponse?>
with ServerStatusRef {
_ServerStatusProviderElement(super.provider);
@override
Uri get baseUrl => (origin as ServerStatusProvider).baseUrl;
@override
void Function(Response, [Object?])? get responseErrorHandler =>
(origin as ServerStatusProvider).responseErrorHandler;
}
String _$fetchContinueListeningHash() => String _$fetchContinueListeningHash() =>
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d'; r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d';
@ -361,7 +521,7 @@ final meProvider = AutoDisposeFutureProvider<User>.internal(
); );
typedef MeRef = AutoDisposeFutureProviderRef<User>; typedef MeRef = AutoDisposeFutureProviderRef<User>;
String _$personalizedViewHash() => r'dada8d72845ffd516f731f88193941f7ebdd47ed'; String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9';
/// fetch the personalized view /// fetch the personalized view
/// ///

View file

@ -52,8 +52,16 @@ class AuthenticatedUser extends _$AuthenticatedUser {
_logger.fine('writing state to box: $state'); _logger.fine('writing state to box: $state');
} }
void addUser(model.AuthenticatedUser user) { void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
state = state..add(user); state = state..add(user);
if (setActive) {
final apiSettings = ref.read(apiSettingsProvider);
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeUser: user,
),
);
}
} }
void removeUsersOfServer(AudiobookShelfServer registeredServer) { void removeUsersOfServer(AudiobookShelfServer registeredServer) {

View file

@ -6,7 +6,7 @@ part of 'authenticated_user_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$authenticatedUserHash() => r'8578d7fda1755ecacce6853076da4149e4ebe3e7'; String _$authenticatedUserHash() => r'308f19b33ae04af6340fb83167fa64aa23400a09';
/// provides with a set of authenticated users /// provides with a set of authenticated users
/// ///

View file

@ -1,15 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/authenticated_user_provider.dart';
import 'package:vaani/api/server_provider.dart';
import 'package:vaani/features/onboarding/view/user_login.dart'; import 'package:vaani/features/onboarding/view/user_login.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/models.dart' as model;
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
import 'package:vaani/shared/widgets/add_new_server.dart'; import 'package:vaani/shared/widgets/add_new_server.dart';
@ -26,80 +21,22 @@ class OnboardingSinglePage extends HookConsumerWidget {
); );
var audiobookshelfUri = makeBaseUrl(serverUriController.text); var audiobookshelfUri = makeBaseUrl(serverUriController.text);
final api = ref.watch(audiobookshelfApiProvider(audiobookshelfUri)); final canUserLogin = useState(apiSettings.activeServer != null);
final isUserLoginAvailable = useState(apiSettings.activeServer != null); fadeSlideTransitionBuilder(
Widget child,
final usernameController = useTextEditingController(); Animation<double> animation,
final passwordController = useTextEditingController(); ) {
return FadeTransition(
void addServer() { opacity: animation,
var newServer = serverUriController.text.isEmpty child: SlideTransition(
? null position: Tween<Offset>(
: model.AudiobookShelfServer( begin: const Offset(0, 0.3),
serverUrl: Uri.parse(serverUriController.text), end: const Offset(0, 0),
); ).animate(animation),
try { child: child,
// 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,
),
);
}
}
/// Login to the server and save the user
Future<void> loginAndSave() async {
final username = usernameController.text;
final password = passwordController.text;
final success = await api.login(username: username, password: password);
if (success != null) {
// save the server
addServer();
var authenticatedUser = model.AuthenticatedUser(
server: model.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,
),
);
// redirect to the library page
GoRouter.of(context).goNamed(Routes.home.name);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login failed. Please check your credentials.'),
),
);
// give focus back to the username field
}
} }
return Scaffold( return Scaffold(
@ -118,9 +55,20 @@ class OnboardingSinglePage extends HookConsumerWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: AnimatedSwitcher(
'Please enter the URL of your AudiobookShelf Server', duration: 500.ms,
style: Theme.of(context).textTheme.bodyMedium, transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value
? Text(
'Server connected, please login',
key: const ValueKey('connected'),
style: Theme.of(context).textTheme.bodyMedium,
)
: Text(
'Please enter the URL of your AudiobookShelf Server',
key: const ValueKey('not_connected'),
style: Theme.of(context).textTheme.bodyMedium,
),
), ),
), ),
Padding( Padding(
@ -129,30 +77,16 @@ class OnboardingSinglePage extends HookConsumerWidget {
controller: serverUriController, controller: serverUriController,
allowEmpty: true, allowEmpty: true,
onPressed: () { onPressed: () {
isUserLoginAvailable.value = canUserLogin.value = serverUriController.text.isNotEmpty;
serverUriController.text.isNotEmpty;
}, },
), ),
), ),
AnimatedSwitcher( AnimatedSwitcher(
duration: 500.ms, duration: 500.ms,
transitionBuilder: (child, animation) { transitionBuilder: fadeSlideTransitionBuilder,
return FadeTransition( child: canUserLogin.value
opacity: animation, ? UserLoginWidget(
child: SlideTransition( server: audiobookshelfUri,
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
).animate(animation),
child: child,
),
);
},
child: isUserLoginAvailable.value
? UserLogin(
passwordController: passwordController,
usernameController: usernameController,
onPressed: loginAndSave,
) )
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0) // ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
: const RedirectToABS().animate().fadeIn().slideY( : const RedirectToABS().animate().fadeIn().slideY(

View file

@ -1,30 +1,343 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/authenticated_user_provider.dart';
import 'package:vaani/api/server_provider.dart';
import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
import 'package:vaani/models/error_response.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/models.dart' as model;
class UserLogin extends HookConsumerWidget { class UserLoginWidget extends HookConsumerWidget {
UserLogin({ UserLoginWidget({
super.key, super.key,
this.usernameController, required this.server,
this.passwordController, });
final Uri server;
final serverStatusError = ErrorResponse();
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverStatus =
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
final api = ref.watch(audiobookshelfApiProvider(server));
return serverStatus.when(
data: (value) {
if (value == null) {
// check the error message
return Text(serverStatusError.response.body);
}
// check available authentication methods and return the correct widget
return UserLoginMultipleAuth(
server: server,
localAvailable:
value.authMethods?.contains(AuthMethod.local) ?? false,
openidAvailable:
value.authMethods?.contains(AuthMethod.openid) ?? false,
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
error: (error, _) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Server is not reachable: $error'),
ElevatedButton(
onPressed: () {
ref.invalidate(
serverStatusProvider(
server,
serverStatusError.storeError,
),
);
},
child: const Text('Try again'),
),
],
),
);
},
);
}
}
enum AuthMethodChoice {
local,
openid,
authToken,
}
class UserLoginMultipleAuth extends HookConsumerWidget {
const UserLoginMultipleAuth({
super.key,
required this.server,
this.localAvailable = false,
this.openidAvailable = false,
this.onPressed, this.onPressed,
}); });
TextEditingController? usernameController; final Uri server;
TextEditingController? passwordController; final bool localAvailable;
final bool openidAvailable;
final void Function()? onPressed; final void Function()? onPressed;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
usernameController ??= useTextEditingController(); // will show choice chips for the available authentication methods
passwordController ??= useTextEditingController(); // authToken method is always available
final methodChoice = useState<AuthMethodChoice>(
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
);
final apiSettings = ref.watch(apiSettingsProvider);
model.AudiobookShelfServer addServer() {
var newServer = model.AudiobookShelfServer(
serverUrl: server,
);
try {
// add the server to the list of servers
ref.read(audiobookShelfServerProvider.notifier).addServer(
newServer,
);
} on ServerAlreadyExistsException catch (e) {
newServer = e.server;
} finally {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeServer: newServer,
),
);
}
return newServer;
}
return Center(
child: InactiveFocusScopeObserver(
child: AutofillGroup(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Wrap(
// mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
runAlignment: WrapAlignment.center,
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
// a small label to show the user what to do
if (localAvailable)
ChoiceChip(
label: const Text('Local'),
selected: methodChoice.value == AuthMethodChoice.local,
onSelected: (selected) {
if (selected) {
methodChoice.value = AuthMethodChoice.local;
}
},
),
if (openidAvailable)
ChoiceChip(
label: const Text('OpenID'),
selected: methodChoice.value == AuthMethodChoice.openid,
onSelected: (selected) {
if (selected) {
methodChoice.value = AuthMethodChoice.openid;
}
},
),
ChoiceChip(
label: const Text('Token'),
selected:
methodChoice.value == AuthMethodChoice.authToken,
onSelected: (selected) {
if (selected) {
methodChoice.value = AuthMethodChoice.authToken;
}
},
),
],
),
const SizedBox.square(
dimension: 8,
),
switch (methodChoice.value) {
AuthMethodChoice.local => UserLoginWithPassword(
server: server,
addServer: addServer,
),
AuthMethodChoice.openid => _UserLoginWithOpenID(
server: server,
addServer: addServer,
),
AuthMethodChoice.authToken => UserLoginWithToken(
server: server,
addServer: addServer,
),
},
],
),
),
),
),
);
}
}
class _UserLoginWithOpenID extends HookConsumerWidget {
const _UserLoginWithOpenID({
super.key,
required this.server,
required this.addServer,
});
final Uri server;
final model.AudiobookShelfServer Function() addServer;
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: implement build
return const Text('OpenID');
}
}
class UserLoginWithToken extends HookConsumerWidget {
UserLoginWithToken({
super.key,
required this.server,
required this.addServer,
});
final Uri server;
final model.AudiobookShelfServer Function() addServer;
final serverErrorResponse = ErrorResponse();
@override
Widget build(BuildContext context, WidgetRef ref) {
final authTokensController = useTextEditingController();
final api = ref.watch(audiobookshelfApiProvider(server));
Future<void> loginAndSave() async {
api.token = authTokensController.text;
model.AuthenticatedUser? authenticatedUser;
LoginResponse? success;
try {
success = await api.misc.authorize(
responseErrorHandler: serverErrorResponse.storeError,
);
if (success == null) {
throw StateError('No response from server');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})',
),
action: SnackBarAction(
label: 'See Error',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(e.toString()),
),
);
},
),
),
);
return;
}
authenticatedUser = model.AuthenticatedUser(
server: addServer(),
id: success.user.id,
username: success.user.username,
authToken: api.token!,
);
ref
.read(authenticatedUserProvider.notifier)
.addUser(authenticatedUser, setActive: true);
context.goNamed(Routes.home.name);
}
return Form(
child: Column(
children: [
TextFormField(
controller: authTokensController,
autofocus: true,
textInputAction: TextInputAction.done,
maxLines: 10,
minLines: 1,
decoration: InputDecoration(
labelText: 'API Token',
labelStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an API token';
}
return null;
},
onFieldSubmitted: (_) async {
await loginAndSave();
},
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: loginAndSave,
child: const Text('Login'),
),
],
),
);
}
}
// class _UserLoginWithToken extends HookConsumerWidget {
class UserLoginWithPassword extends HookConsumerWidget {
UserLoginWithPassword({
super.key,
required this.server,
required this.addServer,
});
final Uri server;
final model.AudiobookShelfServer Function() addServer;
final serverErrorResponse = ErrorResponse();
@override
Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController();
final passwordController = useTextEditingController();
final isPasswordVisibleAnimationController = useAnimationController( final isPasswordVisibleAnimationController = useAnimationController(
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
var isPasswordVisible = useState(false); var isPasswordVisible = useState(false);
final api = ref.watch(audiobookshelfApiProvider(server));
// forward animation when the password visibility changes // forward animation when the password visibility changes
useEffect( useEffect(
@ -39,6 +352,61 @@ class UserLogin extends HookConsumerWidget {
[isPasswordVisible.value], [isPasswordVisible.value],
); );
/// Login to the server and save the user
Future<void> loginAndSave() async {
final username = usernameController.text;
final password = passwordController.text;
LoginResponse? success;
try {
success = await api.login(
username: username,
password: password,
responseErrorHandler: serverErrorResponse.storeError,
);
if (success == null) {
throw StateError('No response from server');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})',
),
action: SnackBarAction(
label: 'See Error',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(e.toString()),
),
);
},
),
),
);
return;
}
// save the server
final authenticatedUser = model.AuthenticatedUser(
server: addServer(),
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, setActive: true);
// redirect to the library page
GoRouter.of(context).goNamed(Routes.home.name);
}
return Center( return Center(
child: InactiveFocusScopeObserver( child: InactiveFocusScopeObserver(
child: AutofillGroup( child: AutofillGroup(
@ -69,11 +437,9 @@ class UserLogin extends HookConsumerWidget {
autofillHints: const [AutofillHints.password], autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
obscureText: !isPasswordVisible.value, obscureText: !isPasswordVisible.value,
onFieldSubmitted: onPressed != null onFieldSubmitted: (_) {
? (_) { loginAndSave();
onPressed!(); },
}
: null,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Password',
labelStyle: TextStyle( labelStyle: TextStyle(
@ -109,7 +475,7 @@ class UserLogin extends HookConsumerWidget {
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
ElevatedButton( ElevatedButton(
onPressed: onPressed, onPressed: loginAndSave,
child: const Text('Login'), child: const Text('Login'),
), ),
], ],

View file

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/authenticated_user_provider.dart'; import 'package:vaani/api/authenticated_user_provider.dart';
import 'package:vaani/api/server_provider.dart'; import 'package:vaani/api/server_provider.dart';
import 'package:vaani/models/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/settings/models/models.dart' as model;
@ -227,7 +228,7 @@ class ServerManagerPage extends HookConsumerWidget {
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
try { try {
final newServer = model.AudiobookShelfServer( final newServer = model.AudiobookShelfServer(
serverUrl: Uri.parse(serverURIController.text), serverUrl: makeBaseUrl(serverURIController.text),
); );
ref ref
.read(audiobookShelfServerProvider.notifier) .read(audiobookShelfServerProvider.notifier)
@ -285,12 +286,16 @@ class _AddUserDialog extends HookConsumerWidget {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final serverErrorResponse = ErrorResponse();
/// Login to the server and save the user /// Login to the server and save the user
Future<model.AuthenticatedUser?> loginAndSave() async { Future<model.AuthenticatedUser?> loginAndSave() async {
model.AuthenticatedUser? authenticatedUser; model.AuthenticatedUser? authenticatedUser;
if (isMethodAuth.value) { if (isMethodAuth.value) {
api.token = authTokensController.text; api.token = authTokensController.text;
final success = await api.misc.authorize(); final success = await api.misc.authorize(
responseErrorHandler: serverErrorResponse.storeError,
);
if (success != null) { if (success != null) {
authenticatedUser = model.AuthenticatedUser( authenticatedUser = model.AuthenticatedUser(
server: server, server: server,
@ -302,7 +307,11 @@ class _AddUserDialog extends HookConsumerWidget {
} else { } else {
final username = usernameController.text; final username = usernameController.text;
final password = passwordController.text; final password = passwordController.text;
final success = await api.login(username: username, password: password); final success = await api.login(
username: username,
password: password,
responseErrorHandler: serverErrorResponse.storeError,
);
if (success != null) { if (success != null) {
authenticatedUser = model.AuthenticatedUser( authenticatedUser = model.AuthenticatedUser(
server: server, server: server,
@ -317,8 +326,10 @@ class _AddUserDialog extends HookConsumerWidget {
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser); ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Login failed. Please check your credentials.'), content: Text(
'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})',
),
), ),
); );
} }
@ -326,7 +337,23 @@ class _AddUserDialog extends HookConsumerWidget {
} }
return AlertDialog( return AlertDialog(
title: const Text('Add User'), // title: Text('Add User for ${server.serverUrl}'),
title: Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Add User for ',
style: Theme.of(context).textTheme.labelLarge,
),
TextSpan(
text: server.serverUrl.toString(),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
content: Form( content: Form(
key: formKey, key: formKey,
child: Column( child: Column(

View file

@ -0,0 +1,21 @@
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
final _logger = Logger('ErrorResponse');
class ErrorResponse {
String? name;
http.Response _response;
ErrorResponse({
this.name,
http.Response? response,
}) : _response = response ?? http.Response('', 418);
void storeError(http.Response response, [Object? error]) {
_logger.warning('for $name got response: $response');
_response = response;
}
http.Response get response => _response;
}

View file

@ -39,6 +39,23 @@ class HomePage extends HookConsumerWidget {
body: Container( body: Container(
child: views.when( child: views.when(
data: (data) { data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('No shelves to display'),
// try again button
ElevatedButton(
onPressed: () {
ref.invalidate(personalizedViewProvider);
},
child: const Text('Try again'),
),
],
),
);
}
final shelvesToDisplay = data final shelvesToDisplay = data
// .where((element) => !element.id.contains('discover')) // .where((element) => !element.id.contains('discover'))
.map((shelf) { .map((shelf) {

View file

@ -20,7 +20,9 @@ mixin _$LibraryItemExtras {
String get heroTagSuffix => throw _privateConstructorUsedError; String get heroTagSuffix => throw _privateConstructorUsedError;
Uint8List? get coverImage => throw _privateConstructorUsedError; Uint8List? get coverImage => throw _privateConstructorUsedError;
@JsonKey(ignore: true) /// Create a copy of LibraryItemExtras
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LibraryItemExtrasCopyWith<LibraryItemExtras> get copyWith => $LibraryItemExtrasCopyWith<LibraryItemExtras> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -44,6 +46,8 @@ class _$LibraryItemExtrasCopyWithImpl<$Res, $Val extends LibraryItemExtras>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of LibraryItemExtras
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -87,6 +91,8 @@ class __$$LibraryItemExtrasImplCopyWithImpl<$Res>
$Res Function(_$LibraryItemExtrasImpl) _then) $Res Function(_$LibraryItemExtrasImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of LibraryItemExtras
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -146,7 +152,9 @@ class _$LibraryItemExtrasImpl implements _LibraryItemExtras {
int get hashCode => Object.hash(runtimeType, book, heroTagSuffix, int get hashCode => Object.hash(runtimeType, book, heroTagSuffix,
const DeepCollectionEquality().hash(coverImage)); const DeepCollectionEquality().hash(coverImage));
@JsonKey(ignore: true) /// Create a copy of LibraryItemExtras
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith => _$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith =>
@ -166,8 +174,11 @@ abstract class _LibraryItemExtras implements LibraryItemExtras {
String get heroTagSuffix; String get heroTagSuffix;
@override @override
Uint8List? get coverImage; Uint8List? get coverImage;
/// Create a copy of LibraryItemExtras
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith => _$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View file

@ -24,8 +24,12 @@ mixin _$ApiSettings {
AuthenticatedUser? get activeUser => throw _privateConstructorUsedError; AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
String? get activeLibraryId => throw _privateConstructorUsedError; String? get activeLibraryId => throw _privateConstructorUsedError;
/// Serializes this ApiSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ApiSettingsCopyWith<ApiSettings> get copyWith => $ApiSettingsCopyWith<ApiSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -55,6 +59,8 @@ class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -78,6 +84,8 @@ class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
) as $Val); ) as $Val);
} }
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res>? get activeServer { $AudiobookShelfServerCopyWith<$Res>? get activeServer {
@ -90,6 +98,8 @@ class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
}); });
} }
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$AuthenticatedUserCopyWith<$Res>? get activeUser { $AuthenticatedUserCopyWith<$Res>? get activeUser {
@ -130,6 +140,8 @@ class __$$ApiSettingsImplCopyWithImpl<$Res>
_$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then) _$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -188,12 +200,14 @@ class _$ApiSettingsImpl implements _ApiSettings {
other.activeLibraryId == activeLibraryId)); other.activeLibraryId == activeLibraryId));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, activeServer, activeUser, activeLibraryId); Object.hash(runtimeType, activeServer, activeUser, activeLibraryId);
@JsonKey(ignore: true) /// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith => _$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
@ -222,8 +236,11 @@ abstract class _ApiSettings implements ApiSettings {
AuthenticatedUser? get activeUser; AuthenticatedUser? get activeUser;
@override @override
String? get activeLibraryId; String? get activeLibraryId;
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith => _$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View file

@ -24,8 +24,12 @@ mixin _$AppSettings {
PlayerSettings get playerSettings => throw _privateConstructorUsedError; PlayerSettings get playerSettings => throw _privateConstructorUsedError;
DownloadSettings get downloadSettings => throw _privateConstructorUsedError; DownloadSettings get downloadSettings => throw _privateConstructorUsedError;
/// Serializes this AppSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AppSettingsCopyWith<AppSettings> get copyWith => $AppSettingsCopyWith<AppSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -56,6 +60,8 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -79,6 +85,8 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
) as $Val); ) as $Val);
} }
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$ThemeSettingsCopyWith<$Res> get themeSettings { $ThemeSettingsCopyWith<$Res> get themeSettings {
@ -87,6 +95,8 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
}); });
} }
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$PlayerSettingsCopyWith<$Res> get playerSettings { $PlayerSettingsCopyWith<$Res> get playerSettings {
@ -95,6 +105,8 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
}); });
} }
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$DownloadSettingsCopyWith<$Res> get downloadSettings { $DownloadSettingsCopyWith<$Res> get downloadSettings {
@ -133,6 +145,8 @@ class __$$AppSettingsImplCopyWithImpl<$Res>
_$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then) _$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -196,12 +210,14 @@ class _$AppSettingsImpl implements _AppSettings {
other.downloadSettings == downloadSettings)); other.downloadSettings == downloadSettings));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, themeSettings, playerSettings, downloadSettings); Object.hash(runtimeType, themeSettings, playerSettings, downloadSettings);
@JsonKey(ignore: true) /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
@ -230,8 +246,11 @@ abstract class _AppSettings implements AppSettings {
PlayerSettings get playerSettings; PlayerSettings get playerSettings;
@override @override
DownloadSettings get downloadSettings; DownloadSettings get downloadSettings;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -247,8 +266,12 @@ mixin _$ThemeSettings {
bool get useCurrentPlayerThemeThroughoutApp => bool get useCurrentPlayerThemeThroughoutApp =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this ThemeSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of ThemeSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ThemeSettingsCopyWith<ThemeSettings> get copyWith => $ThemeSettingsCopyWith<ThemeSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -275,6 +298,8 @@ class _$ThemeSettingsCopyWithImpl<$Res, $Val extends ThemeSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of ThemeSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -322,6 +347,8 @@ class __$$ThemeSettingsImplCopyWithImpl<$Res>
_$ThemeSettingsImpl _value, $Res Function(_$ThemeSettingsImpl) _then) _$ThemeSettingsImpl _value, $Res Function(_$ThemeSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of ThemeSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -390,12 +417,14 @@ class _$ThemeSettingsImpl implements _ThemeSettings {
useCurrentPlayerThemeThroughoutApp)); useCurrentPlayerThemeThroughoutApp));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, isDarkMode, int get hashCode => Object.hash(runtimeType, isDarkMode,
useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp); useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp);
@JsonKey(ignore: true) /// Create a copy of ThemeSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith => _$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith =>
@ -424,8 +453,11 @@ abstract class _ThemeSettings implements ThemeSettings {
bool get useMaterialThemeOnItemPage; bool get useMaterialThemeOnItemPage;
@override @override
bool get useCurrentPlayerThemeThroughoutApp; bool get useCurrentPlayerThemeThroughoutApp;
/// Create a copy of ThemeSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith => _$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -447,8 +479,12 @@ mixin _$PlayerSettings {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
Duration get playbackReportInterval => throw _privateConstructorUsedError; Duration get playbackReportInterval => throw _privateConstructorUsedError;
/// Serializes this PlayerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$PlayerSettingsCopyWith<PlayerSettings> get copyWith => $PlayerSettingsCopyWith<PlayerSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -483,6 +519,8 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -526,6 +564,8 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
) as $Val); ) as $Val);
} }
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings { $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings {
@ -535,6 +575,8 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
}); });
} }
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings { $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings {
@ -544,6 +586,8 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
}); });
} }
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings { $SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings {
@ -587,6 +631,8 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
_$PlayerSettingsImpl _value, $Res Function(_$PlayerSettingsImpl) _then) _$PlayerSettingsImpl _value, $Res Function(_$PlayerSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -701,7 +747,7 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
other.playbackReportInterval == playbackReportInterval)); other.playbackReportInterval == playbackReportInterval));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -713,7 +759,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
sleepTimerSettings, sleepTimerSettings,
playbackReportInterval); playbackReportInterval);
@JsonKey(ignore: true) /// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith =>
@ -755,8 +803,11 @@ abstract class _PlayerSettings implements PlayerSettings {
SleepTimerSettings get sleepTimerSettings; SleepTimerSettings get sleepTimerSettings;
@override @override
Duration get playbackReportInterval; Duration get playbackReportInterval;
/// Create a copy of PlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -771,8 +822,12 @@ mixin _$ExpandedPlayerSettings {
bool get showTotalProgress => throw _privateConstructorUsedError; bool get showTotalProgress => throw _privateConstructorUsedError;
bool get showChapterProgress => throw _privateConstructorUsedError; bool get showChapterProgress => throw _privateConstructorUsedError;
/// Serializes this ExpandedPlayerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of ExpandedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ExpandedPlayerSettingsCopyWith<ExpandedPlayerSettings> get copyWith => $ExpandedPlayerSettingsCopyWith<ExpandedPlayerSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -797,6 +852,8 @@ class _$ExpandedPlayerSettingsCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of ExpandedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -838,6 +895,8 @@ class __$$ExpandedPlayerSettingsImplCopyWithImpl<$Res>
$Res Function(_$ExpandedPlayerSettingsImpl) _then) $Res Function(_$ExpandedPlayerSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of ExpandedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -889,12 +948,14 @@ class _$ExpandedPlayerSettingsImpl implements _ExpandedPlayerSettings {
other.showChapterProgress == showChapterProgress)); other.showChapterProgress == showChapterProgress));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, showTotalProgress, showChapterProgress); Object.hash(runtimeType, showTotalProgress, showChapterProgress);
@JsonKey(ignore: true) /// Create a copy of ExpandedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl> _$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl>
@ -921,8 +982,11 @@ abstract class _ExpandedPlayerSettings implements ExpandedPlayerSettings {
bool get showTotalProgress; bool get showTotalProgress;
@override @override
bool get showChapterProgress; bool get showChapterProgress;
/// Create a copy of ExpandedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl> _$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -936,8 +1000,12 @@ MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson(
mixin _$MinimizedPlayerSettings { mixin _$MinimizedPlayerSettings {
bool get useChapterInfo => throw _privateConstructorUsedError; bool get useChapterInfo => throw _privateConstructorUsedError;
/// Serializes this MinimizedPlayerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of MinimizedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MinimizedPlayerSettingsCopyWith<MinimizedPlayerSettings> get copyWith => $MinimizedPlayerSettingsCopyWith<MinimizedPlayerSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -962,6 +1030,8 @@ class _$MinimizedPlayerSettingsCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of MinimizedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -998,6 +1068,8 @@ class __$$MinimizedPlayerSettingsImplCopyWithImpl<$Res>
$Res Function(_$MinimizedPlayerSettingsImpl) _then) $Res Function(_$MinimizedPlayerSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of MinimizedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1038,11 +1110,13 @@ class _$MinimizedPlayerSettingsImpl implements _MinimizedPlayerSettings {
other.useChapterInfo == useChapterInfo)); other.useChapterInfo == useChapterInfo));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, useChapterInfo); int get hashCode => Object.hash(runtimeType, useChapterInfo);
@JsonKey(ignore: true) /// Create a copy of MinimizedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl> _$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl>
@ -1066,8 +1140,11 @@ abstract class _MinimizedPlayerSettings implements MinimizedPlayerSettings {
@override @override
bool get useChapterInfo; bool get useChapterInfo;
/// Create a copy of MinimizedPlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl> _$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1109,8 +1186,12 @@ mixin _$SleepTimerSettings {
Duration get autoTurnOnTime => throw _privateConstructorUsedError; Duration get autoTurnOnTime => throw _privateConstructorUsedError;
Duration get autoTurnOffTime => throw _privateConstructorUsedError; Duration get autoTurnOffTime => throw _privateConstructorUsedError;
/// Serializes this SleepTimerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SleepTimerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SleepTimerSettingsCopyWith<SleepTimerSettings> get copyWith => $SleepTimerSettingsCopyWith<SleepTimerSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -1147,6 +1228,8 @@ class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SleepTimerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1253,6 +1336,8 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res>
$Res Function(_$SleepTimerSettingsImpl) _then) $Res Function(_$SleepTimerSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SleepTimerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1456,7 +1541,7 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
other.autoTurnOffTime == autoTurnOffTime)); other.autoTurnOffTime == autoTurnOffTime));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -1474,7 +1559,9 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
autoTurnOnTime, autoTurnOnTime,
autoTurnOffTime); autoTurnOffTime);
@JsonKey(ignore: true) /// Create a copy of SleepTimerSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith => _$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith =>
@ -1512,10 +1599,10 @@ abstract class _SleepTimerSettings implements SleepTimerSettings {
Duration get defaultDuration; Duration get defaultDuration;
@override @override
SleepTimerShakeSenseMode get shakeSenseMode; SleepTimerShakeSenseMode get shakeSenseMode;
@override
/// the duration in which the shake is detected before the end of the timer and after the timer ends /// the duration in which the shake is detected before the end of the timer and after the timer ends
/// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds]
@override
Duration get shakeSenseDuration; Duration get shakeSenseDuration;
@override @override
bool get vibrateWhenReset; bool get vibrateWhenReset;
@ -1525,32 +1612,35 @@ abstract class _SleepTimerSettings implements SleepTimerSettings {
bool get fadeOutAudio; bool get fadeOutAudio;
@override @override
double get shakeDetectThreshold; double get shakeDetectThreshold;
@override
/// if true, the player will automatically rewind the audio when the sleep timer is stopped /// if true, the player will automatically rewind the audio when the sleep timer is stopped
bool get autoRewindWhenStopped;
@override @override
bool get autoRewindWhenStopped;
/// the key is the duration in minutes /// the key is the duration in minutes
Map<int, Duration> get autoRewindDurations;
@override @override
Map<int, Duration> get autoRewindDurations;
/// auto turn on timer settings /// auto turn on timer settings
bool get autoTurnOnTimer;
@override @override
bool get autoTurnOnTimer;
/// always auto turn on timer settings or during specific times /// always auto turn on timer settings or during specific times
bool get alwaysAutoTurnOnTimer;
@override @override
bool get alwaysAutoTurnOnTimer;
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false /// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
/// ///
/// duration is the time from 00:00 /// duration is the time from 00:00
@override
Duration get autoTurnOnTime; Duration get autoTurnOnTime;
@override @override
Duration get autoTurnOffTime; Duration get autoTurnOffTime;
/// Create a copy of SleepTimerSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith => _$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -1568,8 +1658,12 @@ mixin _$DownloadSettings {
int get maxConcurrentByHost => throw _privateConstructorUsedError; int get maxConcurrentByHost => throw _privateConstructorUsedError;
int get maxConcurrentByGroup => throw _privateConstructorUsedError; int get maxConcurrentByGroup => throw _privateConstructorUsedError;
/// Serializes this DownloadSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of DownloadSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$DownloadSettingsCopyWith<DownloadSettings> get copyWith => $DownloadSettingsCopyWith<DownloadSettings> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -1599,6 +1693,8 @@ class _$DownloadSettingsCopyWithImpl<$Res, $Val extends DownloadSettings>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of DownloadSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1663,6 +1759,8 @@ class __$$DownloadSettingsImplCopyWithImpl<$Res>
$Res Function(_$DownloadSettingsImpl) _then) $Res Function(_$DownloadSettingsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of DownloadSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1758,12 +1856,14 @@ class _$DownloadSettingsImpl implements _DownloadSettings {
other.maxConcurrentByGroup == maxConcurrentByGroup)); other.maxConcurrentByGroup == maxConcurrentByGroup));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, requiresWiFi, retries, int get hashCode => Object.hash(runtimeType, requiresWiFi, retries,
allowPause, maxConcurrent, maxConcurrentByHost, maxConcurrentByGroup); allowPause, maxConcurrent, maxConcurrentByHost, maxConcurrentByGroup);
@JsonKey(ignore: true) /// Create a copy of DownloadSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith => _$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith =>
@ -1802,8 +1902,11 @@ abstract class _DownloadSettings implements DownloadSettings {
int get maxConcurrentByHost; int get maxConcurrentByHost;
@override @override
int get maxConcurrentByGroup; int get maxConcurrentByGroup;
/// Create a copy of DownloadSettings
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith => _$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View file

@ -22,8 +22,12 @@ AudiobookShelfServer _$AudiobookShelfServerFromJson(Map<String, dynamic> json) {
mixin _$AudiobookShelfServer { mixin _$AudiobookShelfServer {
Uri get serverUrl => throw _privateConstructorUsedError; Uri get serverUrl => throw _privateConstructorUsedError;
/// Serializes this AudiobookShelfServer to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith => $AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -48,6 +52,8 @@ class _$AudiobookShelfServerCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -81,6 +87,8 @@ class __$$AudiobookShelfServerImplCopyWithImpl<$Res>
$Res Function(_$AudiobookShelfServerImpl) _then) $Res Function(_$AudiobookShelfServerImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -120,11 +128,13 @@ class _$AudiobookShelfServerImpl implements _AudiobookShelfServer {
other.serverUrl == serverUrl)); other.serverUrl == serverUrl));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, serverUrl); int get hashCode => Object.hash(runtimeType, serverUrl);
@JsonKey(ignore: true) /// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl> _$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
@ -149,8 +159,11 @@ abstract class _AudiobookShelfServer implements AudiobookShelfServer {
@override @override
Uri get serverUrl; Uri get serverUrl;
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl> _$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }

View file

@ -26,8 +26,12 @@ mixin _$AuthenticatedUser {
String? get username => throw _privateConstructorUsedError; String? get username => throw _privateConstructorUsedError;
String? get password => throw _privateConstructorUsedError; String? get password => throw _privateConstructorUsedError;
/// Serializes this AuthenticatedUser to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith => $AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -58,6 +62,8 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -91,6 +97,8 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
) as $Val); ) as $Val);
} }
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res> get server { $AudiobookShelfServerCopyWith<$Res> get server {
@ -127,6 +135,8 @@ class __$$AuthenticatedUserImplCopyWithImpl<$Res>
$Res Function(_$AuthenticatedUserImpl) _then) $Res Function(_$AuthenticatedUserImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -205,12 +215,14 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser {
other.password == password)); other.password == password));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, server, authToken, id, username, password); Object.hash(runtimeType, server, authToken, id, username, password);
@JsonKey(ignore: true) /// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith => _$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
@ -246,8 +258,11 @@ abstract class _AuthenticatedUser implements AuthenticatedUser {
String? get username; String? get username;
@override @override
String? get password; String? get password;
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith => _$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View file

@ -47,39 +47,7 @@ class AddNewServer extends HookConsumerWidget {
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: 'https://', prefixText: 'https://',
prefixIcon: Tooltip( prefixIcon: ServerAliveIcon(server: Uri.parse(newServerURI.text)),
message: newServerURI.text.isEmpty
? 'Server Status'
: isServerAliveValue
? 'Server connected'
: 'Cannot connect to server',
child: newServerURI.text.isEmpty
? Icon(
Icons.cloud_outlined,
color: Theme.of(context).colorScheme.onSurface,
)
: isServerAlive.when(
data: (value) {
return value
? Icon(
Icons.cloud_done_outlined,
color: Theme.of(context).colorScheme.primary,
)
: Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
);
},
loading: () => Transform.scale(
scale: 0.5,
child: const CircularProgressIndicator(),
),
error: (error, _) => Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
),
),
),
// add server button // add server button
suffixIcon: onPressed == null suffixIcon: onPressed == null
@ -105,3 +73,56 @@ class AddNewServer extends HookConsumerWidget {
// add to add to existing servers // add to add to existing servers
} }
} }
class ServerAliveIcon extends HookConsumerWidget {
const ServerAliveIcon({
super.key,
required this.server,
});
final Uri server;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isServerAlive = ref.watch(isServerAliveProvider(server.toString()));
bool isServerAliveValue = isServerAlive.when(
data: (value) => value,
loading: () => false,
error: (error, _) => false,
);
return Tooltip(
message: server.toString().isEmpty
? 'Server Status'
: isServerAliveValue
? 'Server connected'
: 'Cannot connect to server',
child: server.toString().isEmpty
? Icon(
Icons.cloud_outlined,
color: Theme.of(context).colorScheme.onSurface,
)
: isServerAlive.when(
data: (value) {
return value
? Icon(
Icons.cloud_done_outlined,
color: Theme.of(context).colorScheme.primary,
)
: Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
);
},
loading: () => Transform.scale(
scale: 0.5,
child: const CircularProgressIndicator(),
),
error: (error, _) => Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
),
),
);
}
}

View file

@ -202,10 +202,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.0" version: "3.4.1"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -218,10 +218,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -410,10 +410,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_picker name: file_picker
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.7" version: "8.1.2"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -479,10 +479,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.21" version: "2.0.22"
flutter_riverpod: flutter_riverpod:
dependency: transitive dependency: transitive
description: description:
@ -561,10 +561,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "48d03a1e4887b00fe622695139246e3c778ac814eeb32421467b56d23fa64034" sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.6" version: "14.2.7"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -697,10 +697,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: just_audio name: just_audio
sha256: ee50602364ba83fa6308f5512dd560c713ec3e1f2bc75f0db43618f0d82ef71a sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.39" version: "0.9.40"
just_audio_background: just_audio_background:
dependency: "direct main" dependency: "direct main"
description: description:
@ -729,10 +729,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: just_audio_web name: just_audio_web
sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.11" version: "0.4.12"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -849,10 +849,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
miniplayer: miniplayer:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1107,7 +1107,7 @@ packages:
description: description:
path: "." path: "."
ref: main ref: main
resolved-ref: de1ca8c4b1ec83ceafa285558244922959fe447a resolved-ref: f5e7db74c2aa367179f5b369407f88848a020c24
url: "https://github.com/Dr-Blank/shelfsdk" url: "https://github.com/Dr-Blank/shelfsdk"
source: git source: git
version: "1.0.0" version: "1.0.0"
@ -1384,10 +1384,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1" version: "1.0.0"
web_socket: web_socket:
dependency: transitive dependency: transitive
description: description:

View file

@ -92,7 +92,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
freezed: ^2.5.2 freezed: ^2.5.2
json_serializable: ^6.8.0 json_serializable: ^6.8.0
riverpod_generator: ^2.4.0 riverpod_generator: ^2.4.2
riverpod_lint: ^2.3.10 riverpod_lint: ^2.3.10
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the