mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-01-05 09:49:32 +00:00
something
This commit is contained in:
parent
dbf4ce1959
commit
a720c977c2
115 changed files with 8819 additions and 1 deletions
143
lib/api/api_provider.dart
Normal file
143
lib/api/api_provider.dart
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// provider to provide the api instance
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/db/cache_manager.dart';
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
|
||||
part 'api_provider.g.dart';
|
||||
|
||||
Uri makeBaseUrl(String address) {
|
||||
if (!address.startsWith('http') && !address.startsWith('https')) {
|
||||
address = 'https://$address';
|
||||
}
|
||||
if (!Uri.parse(address).isAbsolute) {
|
||||
throw ArgumentError.value(address, 'address', 'Invalid address');
|
||||
}
|
||||
return Uri.parse(address);
|
||||
}
|
||||
|
||||
/// get the api instance for the given base url
|
||||
@riverpod
|
||||
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
|
||||
// try to get the base url from app settings
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
||||
if (baseUrl == null) {
|
||||
throw ArgumentError.notNull('baseUrl');
|
||||
}
|
||||
return AudiobookshelfApi(
|
||||
baseUrl: baseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
/// get the api instance for the authenticated user
|
||||
///
|
||||
/// if the user is not authenticated throw an error
|
||||
@riverpod
|
||||
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
final user = apiSettings.activeUser;
|
||||
if (user == null) {
|
||||
throw StateError('No active user');
|
||||
}
|
||||
return AudiobookshelfApi(
|
||||
baseUrl: Uri.https(user.server.serverUrl.toString()),
|
||||
token: user.authToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// ping the server to check if it is reachable
|
||||
@riverpod
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
if (!address.startsWith('http') && !address.startsWith('https')) {
|
||||
address = 'https://$address';
|
||||
}
|
||||
|
||||
// check url is valid
|
||||
if (!Uri.parse(address).isAbsolute) {
|
||||
return false;
|
||||
}
|
||||
return await AudiobookshelfApi(baseUrl: Uri.parse(address)).server.ping() ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// fetch the personalized view
|
||||
@riverpod
|
||||
class PersonalizedView extends _$PersonalizedView {
|
||||
@override
|
||||
Stream<List<Shelf>> build() async* {
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
final user = apiSettings.activeUser;
|
||||
if (apiSettings.activeLibraryId == null) {
|
||||
// set it to default user library by logging in and getting the library id
|
||||
final login =
|
||||
await api.login(username: user!.username!, password: user.password!);
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId),
|
||||
);
|
||||
}
|
||||
// try to find in cache
|
||||
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
||||
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}';
|
||||
final cachedRes = await apiResponseCacheManager.getFileFromCache(
|
||||
key,
|
||||
);
|
||||
if (cachedRes != null) {
|
||||
final resJson = jsonDecode(await cachedRes.file.readAsString()) as List;
|
||||
final res = [
|
||||
for (final item in resJson)
|
||||
Shelf.fromJson(item as Map<String, dynamic>),
|
||||
];
|
||||
debugPrint('reading from cache: $cachedRes');
|
||||
yield res;
|
||||
}
|
||||
|
||||
// ! exagerated delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final res = await api.libraries
|
||||
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
|
||||
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
|
||||
// save to cache
|
||||
final newFile = await apiResponseCacheManager.putFile(
|
||||
key,
|
||||
utf8.encode(jsonEncode(res)),
|
||||
fileExtension: 'json',
|
||||
key: key,
|
||||
);
|
||||
debugPrint('writing to cache: $newFile');
|
||||
yield res!;
|
||||
}
|
||||
|
||||
// method to force refresh the view and ignore the cache
|
||||
Future<void> forceRefresh() async {
|
||||
// clear the cache
|
||||
return apiResponseCacheManager.emptyCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch continue listening audiobooks
|
||||
@riverpod
|
||||
FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
||||
FetchContinueListeningRef ref,
|
||||
) async {
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
final res = await api.me.getSessions();
|
||||
// debugPrint(
|
||||
// 'fetchContinueListening: ${res.sessions.map((e) => e.libraryItemId).toSet()}',
|
||||
// );
|
||||
return res!;
|
||||
}
|
||||
370
lib/api/api_provider.g.dart
Normal file
370
lib/api/api_provider.g.dart
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'api_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$audiobookshelfApiHash() => r'5eb091c6b18c0bf5a0eec079fdb872a84c4f00d9';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
@ProviderFor(audiobookshelfApi)
|
||||
const audiobookshelfApiProvider = AudiobookshelfApiFamily();
|
||||
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
class AudiobookshelfApiFamily extends Family<AudiobookshelfApi> {
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
const AudiobookshelfApiFamily();
|
||||
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
AudiobookshelfApiProvider call(
|
||||
Uri? baseUrl,
|
||||
) {
|
||||
return AudiobookshelfApiProvider(
|
||||
baseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AudiobookshelfApiProvider getProviderOverride(
|
||||
covariant AudiobookshelfApiProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.baseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
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'audiobookshelfApiProvider';
|
||||
}
|
||||
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
class AudiobookshelfApiProvider extends AutoDisposeProvider<AudiobookshelfApi> {
|
||||
/// get the api instance for the given base url
|
||||
///
|
||||
/// Copied from [audiobookshelfApi].
|
||||
AudiobookshelfApiProvider(
|
||||
Uri? baseUrl,
|
||||
) : this._internal(
|
||||
(ref) => audiobookshelfApi(
|
||||
ref as AudiobookshelfApiRef,
|
||||
baseUrl,
|
||||
),
|
||||
from: audiobookshelfApiProvider,
|
||||
name: r'audiobookshelfApiProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$audiobookshelfApiHash,
|
||||
dependencies: AudiobookshelfApiFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
AudiobookshelfApiFamily._allTransitiveDependencies,
|
||||
baseUrl: baseUrl,
|
||||
);
|
||||
|
||||
AudiobookshelfApiProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.baseUrl,
|
||||
}) : super.internal();
|
||||
|
||||
final Uri? baseUrl;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
AudiobookshelfApi Function(AudiobookshelfApiRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: AudiobookshelfApiProvider._internal(
|
||||
(ref) => create(ref as AudiobookshelfApiRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
baseUrl: baseUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<AudiobookshelfApi> createElement() {
|
||||
return _AudiobookshelfApiProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, baseUrl.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
|
||||
/// The parameter `baseUrl` of this provider.
|
||||
Uri? get baseUrl;
|
||||
}
|
||||
|
||||
class _AudiobookshelfApiProviderElement
|
||||
extends AutoDisposeProviderElement<AudiobookshelfApi>
|
||||
with AudiobookshelfApiRef {
|
||||
_AudiobookshelfApiProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
|
||||
}
|
||||
|
||||
String _$authenticatedApiHash() => r'62213d5d0268eeaa2a16211cd60b1b6f0d19dd40';
|
||||
|
||||
/// get the api instance for the authenticated user
|
||||
///
|
||||
/// if the user is not authenticated throw an error
|
||||
///
|
||||
/// Copied from [authenticatedApi].
|
||||
@ProviderFor(authenticatedApi)
|
||||
final authenticatedApiProvider =
|
||||
AutoDisposeProvider<AudiobookshelfApi>.internal(
|
||||
authenticatedApi,
|
||||
name: r'authenticatedApiProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$authenticatedApiHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef AuthenticatedApiRef = AutoDisposeProviderRef<AudiobookshelfApi>;
|
||||
String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a';
|
||||
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
@ProviderFor(isServerAlive)
|
||||
const isServerAliveProvider = IsServerAliveFamily();
|
||||
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
class IsServerAliveFamily extends Family<AsyncValue<bool>> {
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
const IsServerAliveFamily();
|
||||
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
IsServerAliveProvider call(
|
||||
String address,
|
||||
) {
|
||||
return IsServerAliveProvider(
|
||||
address,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IsServerAliveProvider getProviderOverride(
|
||||
covariant IsServerAliveProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.address,
|
||||
);
|
||||
}
|
||||
|
||||
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'isServerAliveProvider';
|
||||
}
|
||||
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
|
||||
/// ping the server to check if it is reachable
|
||||
///
|
||||
/// Copied from [isServerAlive].
|
||||
IsServerAliveProvider(
|
||||
String address,
|
||||
) : this._internal(
|
||||
(ref) => isServerAlive(
|
||||
ref as IsServerAliveRef,
|
||||
address,
|
||||
),
|
||||
from: isServerAliveProvider,
|
||||
name: r'isServerAliveProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$isServerAliveHash,
|
||||
dependencies: IsServerAliveFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
IsServerAliveFamily._allTransitiveDependencies,
|
||||
address: address,
|
||||
);
|
||||
|
||||
IsServerAliveProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.address,
|
||||
}) : super.internal();
|
||||
|
||||
final String address;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<bool> Function(IsServerAliveRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: IsServerAliveProvider._internal(
|
||||
(ref) => create(ref as IsServerAliveRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
address: address,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<bool> createElement() {
|
||||
return _IsServerAliveProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is IsServerAliveProvider && other.address == address;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, address.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
|
||||
/// The parameter `address` of this provider.
|
||||
String get address;
|
||||
}
|
||||
|
||||
class _IsServerAliveProviderElement
|
||||
extends AutoDisposeFutureProviderElement<bool> with IsServerAliveRef {
|
||||
_IsServerAliveProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get address => (origin as IsServerAliveProvider).address;
|
||||
}
|
||||
|
||||
String _$fetchContinueListeningHash() =>
|
||||
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d';
|
||||
|
||||
/// fetch continue listening audiobooks
|
||||
///
|
||||
/// Copied from [fetchContinueListening].
|
||||
@ProviderFor(fetchContinueListening)
|
||||
final fetchContinueListeningProvider =
|
||||
AutoDisposeFutureProvider<GetUserSessionsResponse>.internal(
|
||||
fetchContinueListening,
|
||||
name: r'fetchContinueListeningProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$fetchContinueListeningHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef FetchContinueListeningRef
|
||||
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
|
||||
String _$personalizedViewHash() => r'52a89c46ce668238ca11b5394fd1d14c910947f5';
|
||||
|
||||
/// fetch the personalized view
|
||||
///
|
||||
/// Copied from [PersonalizedView].
|
||||
@ProviderFor(PersonalizedView)
|
||||
final personalizedViewProvider =
|
||||
AutoDisposeStreamNotifierProvider<PersonalizedView, List<Shelf>>.internal(
|
||||
PersonalizedView.new,
|
||||
name: r'personalizedViewProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$personalizedViewHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
79
lib/api/authenticated_user_provider.dart
Normal file
79
lib/api/authenticated_user_provider.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:whispering_pages/api/server_provider.dart'
|
||||
show audiobookShelfServerProvider;
|
||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||
import 'package:whispering_pages/settings/models/authenticated_user.dart' as model;
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
import 'package:whispering_pages/db/storage.dart';
|
||||
|
||||
part 'authenticated_user_provider.g.dart';
|
||||
|
||||
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
||||
|
||||
/// provides with a set of authenticated users
|
||||
@riverpod
|
||||
class AuthenticatedUser extends _$AuthenticatedUser {
|
||||
@override
|
||||
Set<model.AuthenticatedUser> build() {
|
||||
ref.listenSelf((_, __) {
|
||||
writeStateToBox();
|
||||
});
|
||||
// get the app settings
|
||||
final apiSettings = ref.read(apiSettingsProvider);
|
||||
|
||||
final availUsers = readFromBoxOrCreate();
|
||||
if (apiSettings.activeUser != null) {
|
||||
availUsers.add(apiSettings.activeUser!);
|
||||
}
|
||||
return availUsers;
|
||||
}
|
||||
|
||||
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
||||
if (_box.isNotEmpty) {
|
||||
final foundData = _box.getRange(0, _box.length);
|
||||
debugPrint('found users in box: $foundData');
|
||||
return foundData.toSet();
|
||||
} else {
|
||||
debugPrint('no settings found in box');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void writeStateToBox() {
|
||||
_box.clear();
|
||||
if (state.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_box.addAll(state);
|
||||
debugPrint('writing state to box: $state');
|
||||
}
|
||||
|
||||
void addUser(model.AuthenticatedUser user) {
|
||||
state = state..add(user);
|
||||
}
|
||||
|
||||
void removeUsersOfServer(AudiobookShelfServer registeredServer) {
|
||||
state = state.where((user) => user.server != registeredServer).toSet();
|
||||
// remove the server from the server provider
|
||||
final serverProvider = ref.read(audiobookShelfServerProvider);
|
||||
if (serverProvider.contains(registeredServer)) {
|
||||
ref
|
||||
.read(audiobookShelfServerProvider.notifier)
|
||||
.removeServer(registeredServer);
|
||||
}
|
||||
}
|
||||
|
||||
void removeUser(model.AuthenticatedUser user) {
|
||||
state = state.where((u) => u != user).toSet();
|
||||
// also remove the user from the active user
|
||||
final apiSettings = ref.read(apiSettingsProvider);
|
||||
if (apiSettings.activeUser == user) {
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeUser: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
lib/api/authenticated_user_provider.g.dart
Normal file
28
lib/api/authenticated_user_provider.g.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'authenticated_user_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$authenticatedUserHash() => r'5702fb6ab1e83129d57c89ef02a65c5910f2a076';
|
||||
|
||||
/// provides with a set of authenticated users
|
||||
///
|
||||
/// Copied from [AuthenticatedUser].
|
||||
@ProviderFor(AuthenticatedUser)
|
||||
final authenticatedUserProvider = AutoDisposeNotifierProvider<AuthenticatedUser,
|
||||
Set<model.AuthenticatedUser>>.internal(
|
||||
AuthenticatedUser.new,
|
||||
name: r'authenticatedUserProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$authenticatedUserHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AuthenticatedUser = AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
55
lib/api/image_provider.dart
Normal file
55
lib/api/image_provider.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/api/api_provider.dart';
|
||||
import 'package:whispering_pages/db/cache_manager.dart';
|
||||
|
||||
/// provides cover images for the audiobooks
|
||||
///
|
||||
/// is a stream provider that provides cover images first from the cache then from the server
|
||||
/// if the image is not found in the cache, it will be fetched from the server and saved to the cache
|
||||
/// if the image is not found in the server it will throw an error
|
||||
|
||||
part 'image_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CoverImage extends _$CoverImage {
|
||||
@override
|
||||
Stream<Uint8List> build(LibraryItem libraryItem) async* {
|
||||
final api = ref.watch(authenticatedApiProvider);
|
||||
|
||||
// try to get the image from the cache
|
||||
final file = await imageCacheManager.getFileFromCache(libraryItem.id);
|
||||
|
||||
if (file != null) {
|
||||
// if the image is in the cache, yield it
|
||||
yield await file.file.readAsBytes();
|
||||
// return if no need to fetch from the server
|
||||
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
|
||||
return;
|
||||
} else {
|
||||
debugPrint(
|
||||
'cover image stale for ${libraryItem.id}, fetching from the server',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// check if the image is in the cache
|
||||
final coverImage = await api.items.getCover(
|
||||
libraryItemId: libraryItem.id,
|
||||
parameters: const GetImageReqParams(width: 500),
|
||||
);
|
||||
// save the image to the cache
|
||||
final newFile = await imageCacheManager.putFile(
|
||||
libraryItem.id,
|
||||
coverImage ?? Uint8List(0),
|
||||
key: libraryItem.id,
|
||||
);
|
||||
debugPrint(
|
||||
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
|
||||
);
|
||||
yield coverImage ?? Uint8List(0);
|
||||
}
|
||||
}
|
||||
174
lib/api/image_provider.g.dart
Normal file
174
lib/api/image_provider.g.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'image_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$coverImageHash() => r'34c6aaf6831fea198984d22ecdf2c5b74e110891';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$CoverImage
|
||||
extends BuildlessAutoDisposeStreamNotifier<Uint8List> {
|
||||
late final LibraryItem libraryItem;
|
||||
|
||||
Stream<Uint8List> build(
|
||||
LibraryItem libraryItem,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [CoverImage].
|
||||
@ProviderFor(CoverImage)
|
||||
const coverImageProvider = CoverImageFamily();
|
||||
|
||||
/// See also [CoverImage].
|
||||
class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
|
||||
/// See also [CoverImage].
|
||||
const CoverImageFamily();
|
||||
|
||||
/// See also [CoverImage].
|
||||
CoverImageProvider call(
|
||||
LibraryItem libraryItem,
|
||||
) {
|
||||
return CoverImageProvider(
|
||||
libraryItem,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
CoverImageProvider getProviderOverride(
|
||||
covariant CoverImageProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.libraryItem,
|
||||
);
|
||||
}
|
||||
|
||||
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'coverImageProvider';
|
||||
}
|
||||
|
||||
/// See also [CoverImage].
|
||||
class CoverImageProvider
|
||||
extends AutoDisposeStreamNotifierProviderImpl<CoverImage, Uint8List> {
|
||||
/// See also [CoverImage].
|
||||
CoverImageProvider(
|
||||
LibraryItem libraryItem,
|
||||
) : this._internal(
|
||||
() => CoverImage()..libraryItem = libraryItem,
|
||||
from: coverImageProvider,
|
||||
name: r'coverImageProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$coverImageHash,
|
||||
dependencies: CoverImageFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
CoverImageFamily._allTransitiveDependencies,
|
||||
libraryItem: libraryItem,
|
||||
);
|
||||
|
||||
CoverImageProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.libraryItem,
|
||||
}) : super.internal();
|
||||
|
||||
final LibraryItem libraryItem;
|
||||
|
||||
@override
|
||||
Stream<Uint8List> runNotifierBuild(
|
||||
covariant CoverImage notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
libraryItem,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(CoverImage Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: CoverImageProvider._internal(
|
||||
() => create()..libraryItem = libraryItem,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
libraryItem: libraryItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
|
||||
createElement() {
|
||||
return _CoverImageProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CoverImageProvider && other.libraryItem == libraryItem;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, libraryItem.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin CoverImageRef on AutoDisposeStreamNotifierProviderRef<Uint8List> {
|
||||
/// The parameter `libraryItem` of this provider.
|
||||
LibraryItem get libraryItem;
|
||||
}
|
||||
|
||||
class _CoverImageProviderElement
|
||||
extends AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
|
||||
with CoverImageRef {
|
||||
_CoverImageProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
102
lib/api/server_provider.dart
Normal file
102
lib/api/server_provider.dart
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:whispering_pages/api/authenticated_user_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/db/storage.dart';
|
||||
|
||||
part 'server_provider.g.dart';
|
||||
|
||||
final _box = AvailableHiveBoxes.serverBox;
|
||||
|
||||
class ServerAlreadyExistsException implements Exception {
|
||||
final model.AudiobookShelfServer server;
|
||||
|
||||
ServerAlreadyExistsException(this.server);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Server $server already exists';
|
||||
}
|
||||
}
|
||||
|
||||
/// provides with a set of servers added by the user
|
||||
@riverpod
|
||||
class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||
@override
|
||||
Set<model.AudiobookShelfServer> build() {
|
||||
ref.listenSelf((_, __) {
|
||||
writeStateToBox();
|
||||
});
|
||||
// get the app settings
|
||||
final apiSettings = ref.read(apiSettingsProvider);
|
||||
// is default server is present, add it to the set
|
||||
final availableServers = readFromBoxOrCreate();
|
||||
if (apiSettings.activeServer != null) {
|
||||
availableServers.add(apiSettings.activeServer!);
|
||||
}
|
||||
// also add server of the user
|
||||
if (apiSettings.activeUser != null) {
|
||||
availableServers.add(apiSettings.activeUser!.server);
|
||||
}
|
||||
return availableServers;
|
||||
}
|
||||
|
||||
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
|
||||
if (_box.isNotEmpty) {
|
||||
final foundServers = _box.getRange(0, _box.length);
|
||||
debugPrint('found servers in box: $foundServers');
|
||||
return foundServers.whereNotNull().toSet();
|
||||
} else {
|
||||
debugPrint('no settings found in box');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void writeStateToBox() {
|
||||
_box.clear();
|
||||
if (state.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_box.addAll(state);
|
||||
debugPrint('writing state to box: $state');
|
||||
}
|
||||
|
||||
void addServer(model.AudiobookShelfServer server) {
|
||||
if (state.contains(server)) {
|
||||
throw ServerAlreadyExistsException(server);
|
||||
}
|
||||
state = {...state, server};
|
||||
}
|
||||
|
||||
void removeServer(model.AudiobookShelfServer server,
|
||||
{
|
||||
bool removeUsers = false,
|
||||
}) {
|
||||
state = state.where((s) => s != server).toSet();
|
||||
// remove the server from the active server
|
||||
final apiSettings = ref.read(apiSettingsProvider);
|
||||
if (apiSettings.activeServer == server) {
|
||||
ref.read(apiSettingsProvider.notifier).updateState(
|
||||
apiSettings.copyWith(
|
||||
activeServer: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
// remove the users of this server
|
||||
if (removeUsers) {
|
||||
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
|
||||
}
|
||||
}
|
||||
|
||||
// ? this doesn't seem to be useful
|
||||
void updateServer(model.AudiobookShelfServer newServer) {
|
||||
state = state
|
||||
.map(
|
||||
(existingServer) =>
|
||||
existingServer == newServer ? newServer : existingServer,
|
||||
)
|
||||
.toSet();
|
||||
}
|
||||
}
|
||||
30
lib/api/server_provider.g.dart
Normal file
30
lib/api/server_provider.g.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$audiobookShelfServerHash() =>
|
||||
r'f0d645bb42233c59886bc43fdc473897484ceca1';
|
||||
|
||||
/// provides with a set of servers added by the user
|
||||
///
|
||||
/// Copied from [AudiobookShelfServer].
|
||||
@ProviderFor(AudiobookShelfServer)
|
||||
final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
|
||||
AudiobookShelfServer, Set<model.AudiobookShelfServer>>.internal(
|
||||
AudiobookShelfServer.new,
|
||||
name: r'audiobookShelfServerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$audiobookShelfServerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AudiobookShelfServer
|
||||
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
22
lib/db/available_boxes.dart
Normal file
22
lib/db/available_boxes.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:flutter/foundation.dart' show immutable;
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:whispering_pages/settings/models/models.dart';
|
||||
|
||||
@immutable
|
||||
class AvailableHiveBoxes {
|
||||
const AvailableHiveBoxes._();
|
||||
|
||||
/// Box for storing user preferences as [AppSettings]
|
||||
static final userPrefsBox = Hive.box<AppSettings>(name: 'userPrefs');
|
||||
|
||||
/// Box for storing [ApiSettings]
|
||||
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
|
||||
|
||||
/// stores the a list of [AudiobookShelfServer]
|
||||
static final serverBox =
|
||||
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer');
|
||||
|
||||
/// stores the a list of [AuthenticatedUser]
|
||||
static final authenticatedUserBox =
|
||||
Hive.box<AuthenticatedUser>(name: 'authenticatedUser');
|
||||
}
|
||||
1009
lib/db/cache/image.g.dart
vendored
Normal file
1009
lib/db/cache/image.g.dart
vendored
Normal file
File diff suppressed because it is too large
Load diff
39
lib/db/cache/schemas/image.dart
vendored
Normal file
39
lib/db/cache/schemas/image.dart
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:isar/isar.dart';
|
||||
|
||||
part '../image.g.dart';
|
||||
|
||||
/// Represents a cover image for a library item
|
||||
///
|
||||
/// stores 2 paths, one is thumbnail and the other is the full size image
|
||||
/// both are optional
|
||||
/// also stores last fetched date for the image
|
||||
/// Id is passed as a parameter to the collection annotation (the lib_item_id)
|
||||
/// also index the id
|
||||
/// This is because the image is a part of the library item and the library item
|
||||
/// is the parent of the image
|
||||
@Collection(ignore: {'path'})
|
||||
@Name('CacheImage')
|
||||
class Image {
|
||||
@Id()
|
||||
int id;
|
||||
|
||||
String? thumbnailPath;
|
||||
String? imagePath;
|
||||
DateTime lastSaved;
|
||||
|
||||
Image({
|
||||
required this.id,
|
||||
this.thumbnailPath,
|
||||
this.imagePath,
|
||||
}) : lastSaved = DateTime.now();
|
||||
|
||||
/// returns the path to the image
|
||||
String? get path => thumbnailPath ?? imagePath;
|
||||
|
||||
/// automatically updates the last fetched date when saving a new path
|
||||
void updatePath(String? thumbnailPath, String? imagePath) async {
|
||||
this.thumbnailPath = thumbnailPath;
|
||||
this.imagePath = imagePath;
|
||||
lastSaved = DateTime.now();
|
||||
}
|
||||
}
|
||||
19
lib/db/cache_manager.dart
Normal file
19
lib/db/cache_manager.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
final imageCacheManager = CacheManager(
|
||||
Config(
|
||||
'image_cache_manager',
|
||||
stalePeriod: const Duration(days: 365 * 10),
|
||||
repo: JsonCacheInfoRepository(),
|
||||
maxNrOfCacheObjects: 1000,
|
||||
),
|
||||
);
|
||||
|
||||
final apiResponseCacheManager = CacheManager(
|
||||
Config(
|
||||
'api_response_cache_manager',
|
||||
stalePeriod: const Duration(days: 1),
|
||||
repo: JsonCacheInfoRepository(),
|
||||
maxNrOfCacheObjects: 1000,
|
||||
),
|
||||
);
|
||||
26
lib/db/init.dart
Normal file
26
lib/db/init.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// does the initial setup of the storage
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:whispering_pages/settings/constants.dart';
|
||||
|
||||
import 'register_models.dart';
|
||||
|
||||
Future initStorage() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
|
||||
// use whispering_pages as the directory for hive
|
||||
final storageDir = Directory(p.join(
|
||||
dir.path,
|
||||
AppMetadata.appName.toLowerCase().replaceAll(' ', '_'),
|
||||
),
|
||||
);
|
||||
await storageDir.create(recursive: true);
|
||||
|
||||
Hive.defaultDirectory = storageDir.path;
|
||||
|
||||
await registerModels();
|
||||
}
|
||||
22
lib/db/register_models.dart
Normal file
22
lib/db/register_models.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:whispering_pages/settings/models/models.dart';
|
||||
|
||||
// register all models to Hive for serialization
|
||||
Future registerModels() async {
|
||||
Hive.registerAdapter<AppSettings>(
|
||||
'AppSettings',
|
||||
((json) => AppSettings.fromJson(json)),
|
||||
);
|
||||
Hive.registerAdapter<ApiSettings>(
|
||||
'ApiSettings',
|
||||
((json) => ApiSettings.fromJson(json)),
|
||||
);
|
||||
Hive.registerAdapter<AudiobookShelfServer>(
|
||||
'AudiobookShelfServer',
|
||||
((json) => AudiobookShelfServer.fromJson(json)),
|
||||
);
|
||||
Hive.registerAdapter<AuthenticatedUser>(
|
||||
'AuthenticatedUser',
|
||||
((json) => AuthenticatedUser.fromJson(json)),
|
||||
);
|
||||
}
|
||||
3
lib/db/storage.dart
Normal file
3
lib/db/storage.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export 'available_boxes.dart';
|
||||
export 'init.dart';
|
||||
export 'register_models.dart';
|
||||
79
lib/hacks/fix_autofill_losing_focus.dart
Normal file
79
lib/hacks/fix_autofill_losing_focus.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A workaround for the issue where the autofill loses focus on Android
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// InactiveFocusScopeObserver(
|
||||
/// child: FormWithTheFeildsThatMayLooseFocus(),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
// see https://github.com/flutter/flutter/issues/137760#issuecomment-1956816977
|
||||
class InactiveFocusScopeObserver extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const InactiveFocusScopeObserver({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InactiveFocusScopeObserver> createState() =>
|
||||
_InactiveFocusScopeObserverState();
|
||||
}
|
||||
|
||||
class _InactiveFocusScopeObserverState
|
||||
extends State<InactiveFocusScopeObserver> {
|
||||
final FocusScopeNode _focusScope = FocusScopeNode();
|
||||
|
||||
AppLifecycleListener? _listener;
|
||||
FocusNode? _lastFocusedNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_registerListener();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FocusScope(
|
||||
node: _focusScope,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listener?.dispose();
|
||||
_focusScope.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerListener() {
|
||||
/// optional if you want this workaround for any platform and not just for android
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
return;
|
||||
}
|
||||
|
||||
_listener = AppLifecycleListener(
|
||||
onInactive: () {
|
||||
_lastFocusedNode = _focusScope.focusedChild;
|
||||
},
|
||||
onResume: () {
|
||||
_lastFocusedNode = null;
|
||||
},
|
||||
);
|
||||
|
||||
_focusScope.addListener(_onFocusChanged);
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_lastFocusedNode?.hasFocus == false) {
|
||||
_lastFocusedNode?.requestFocus();
|
||||
_lastFocusedNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lib/main.dart
Normal file
44
lib/main.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:whispering_pages/api/server_provider.dart';
|
||||
import 'package:whispering_pages/db/storage.dart';
|
||||
import 'package:whispering_pages/pages/onboarding/onboarding.dart';
|
||||
import 'package:whispering_pages/pages/pages.dart';
|
||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||
import 'package:whispering_pages/theme/theme.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// initialize the storage
|
||||
await initStorage();
|
||||
runApp(const ProviderScope(
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final servers = ref.watch(audiobookShelfServerProvider);
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final apiSettings = ref.watch(apiSettingsProvider);
|
||||
|
||||
bool needOnboarding() {
|
||||
return apiSettings.activeUser == null || servers.isEmpty;
|
||||
}
|
||||
return MaterialApp(
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: ref.watch(appSettingsProvider).isDarkMode
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
home: needOnboarding() ? const OnboardingPage() : const HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
lib/settings/api_settings_provider.dart
Normal file
47
lib/settings/api_settings_provider.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// this provider is used to provide the Api settings to the app
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:whispering_pages/settings/models/api_settings.dart' as model;
|
||||
import 'package:whispering_pages/db/available_boxes.dart';
|
||||
|
||||
part 'api_settings_provider.g.dart';
|
||||
|
||||
final _box = AvailableHiveBoxes.apiSettingsBox;
|
||||
|
||||
@riverpod
|
||||
class ApiSettings extends _$ApiSettings {
|
||||
@override
|
||||
model.ApiSettings build() {
|
||||
state = readFromBoxOrCreate();
|
||||
ref.listenSelf((_, __) {
|
||||
writeToBox();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
model.ApiSettings readFromBoxOrCreate() {
|
||||
// see if the settings are already in the box
|
||||
if (_box.isNotEmpty) {
|
||||
final foundSettings = _box.getAt(0);
|
||||
debugPrint('found api settings in box: $foundSettings');
|
||||
return foundSettings;
|
||||
} else {
|
||||
// create a new settings object
|
||||
const settings = model.ApiSettings();
|
||||
debugPrint('created new api settings: $settings');
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
// write the settings to the box
|
||||
void writeToBox() {
|
||||
_box.clear();
|
||||
_box.add(state);
|
||||
debugPrint('wrote api settings to box: $state');
|
||||
}
|
||||
|
||||
void updateState(model.ApiSettings newSettings) {
|
||||
state = newSettings;
|
||||
}
|
||||
}
|
||||
25
lib/settings/api_settings_provider.g.dart
Normal file
25
lib/settings/api_settings_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'api_settings_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiSettingsHash() => r'a6927751bd91ec7c9e1a2810dc939407d9112210';
|
||||
|
||||
/// See also [ApiSettings].
|
||||
@ProviderFor(ApiSettings)
|
||||
final apiSettingsProvider =
|
||||
AutoDisposeNotifierProvider<ApiSettings, model.ApiSettings>.internal(
|
||||
ApiSettings.new,
|
||||
name: r'apiSettingsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ApiSettings = AutoDisposeNotifier<model.ApiSettings>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
51
lib/settings/app_settings_provider.dart
Normal file
51
lib/settings/app_settings_provider.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// this provider is used to provide the app settings to the app
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
|
||||
import 'package:whispering_pages/db/available_boxes.dart';
|
||||
|
||||
part 'app_settings_provider.g.dart';
|
||||
|
||||
final _box = AvailableHiveBoxes.userPrefsBox;
|
||||
|
||||
@riverpod
|
||||
class AppSettings extends _$AppSettings {
|
||||
@override
|
||||
model.AppSettings build() {
|
||||
state = readFromBoxOrCreate();
|
||||
ref.listenSelf((_, __) {
|
||||
writeToBox();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
model.AppSettings readFromBoxOrCreate() {
|
||||
// see if the settings are already in the box
|
||||
if (_box.isNotEmpty) {
|
||||
final foundSettings = _box.getAt(0);
|
||||
debugPrint('found settings in box: $foundSettings');
|
||||
return foundSettings;
|
||||
} else {
|
||||
// create a new settings object
|
||||
const settings = model.AppSettings();
|
||||
debugPrint('created new settings: $settings');
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
// write the settings to the box
|
||||
void writeToBox() {
|
||||
_box.clear();
|
||||
_box.add(state);
|
||||
debugPrint('wrote settings to box: $state');
|
||||
}
|
||||
|
||||
void toggleDarkMode() {
|
||||
state = state.copyWith(isDarkMode: !state.isDarkMode);
|
||||
}
|
||||
|
||||
void updateState(model.AppSettings newSettings) {
|
||||
state = newSettings;
|
||||
}
|
||||
}
|
||||
25
lib/settings/app_settings_provider.g.dart
Normal file
25
lib/settings/app_settings_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsHash() => r'da2cd1bb0da6e136e906bc61f29da89d0c5f53fb';
|
||||
|
||||
/// See also [AppSettings].
|
||||
@ProviderFor(AppSettings)
|
||||
final appSettingsProvider =
|
||||
AutoDisposeNotifierProvider<AppSettings, model.AppSettings>.internal(
|
||||
AppSettings.new,
|
||||
name: r'appSettingsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AppSettings = AutoDisposeNotifier<model.AppSettings>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
8
lib/settings/constants.dart
Normal file
8
lib/settings/constants.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter/foundation.dart' show immutable;
|
||||
|
||||
|
||||
@immutable
|
||||
class AppMetadata {
|
||||
const AppMetadata._();
|
||||
static const String appName = 'Whispering Pages';
|
||||
}
|
||||
23
lib/settings/models/api_settings.dart
Normal file
23
lib/settings/models/api_settings.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// a freezed class to store the settings of the app
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||
import 'package:whispering_pages/settings/models/authenticated_user.dart';
|
||||
|
||||
part 'api_settings.freezed.dart';
|
||||
part 'api_settings.g.dart';
|
||||
|
||||
/// stores the settings for the active server and user
|
||||
///
|
||||
/// all settings that are needed to interact with the server are stored here
|
||||
@freezed
|
||||
class ApiSettings with _$ApiSettings {
|
||||
const factory ApiSettings({
|
||||
AudiobookShelfServer? activeServer,
|
||||
AuthenticatedUser? activeUser,
|
||||
String? activeLibraryId,
|
||||
}) = _ApiSettings;
|
||||
|
||||
factory ApiSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$ApiSettingsFromJson(json);
|
||||
}
|
||||
229
lib/settings/models/api_settings.freezed.dart
Normal file
229
lib/settings/models/api_settings.freezed.dart
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'api_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
ApiSettings _$ApiSettingsFromJson(Map<String, dynamic> json) {
|
||||
return _ApiSettings.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ApiSettings {
|
||||
AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError;
|
||||
AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
|
||||
String? get activeLibraryId => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$ApiSettingsCopyWith<ApiSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ApiSettingsCopyWith<$Res> {
|
||||
factory $ApiSettingsCopyWith(
|
||||
ApiSettings value, $Res Function(ApiSettings) then) =
|
||||
_$ApiSettingsCopyWithImpl<$Res, ApiSettings>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{AudiobookShelfServer? activeServer,
|
||||
AuthenticatedUser? activeUser,
|
||||
String? activeLibraryId});
|
||||
|
||||
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
|
||||
$AuthenticatedUserCopyWith<$Res>? get activeUser;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
|
||||
implements $ApiSettingsCopyWith<$Res> {
|
||||
_$ApiSettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? activeServer = freezed,
|
||||
Object? activeUser = freezed,
|
||||
Object? activeLibraryId = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
activeServer: freezed == activeServer
|
||||
? _value.activeServer
|
||||
: activeServer // ignore: cast_nullable_to_non_nullable
|
||||
as AudiobookShelfServer?,
|
||||
activeUser: freezed == activeUser
|
||||
? _value.activeUser
|
||||
: activeUser // ignore: cast_nullable_to_non_nullable
|
||||
as AuthenticatedUser?,
|
||||
activeLibraryId: freezed == activeLibraryId
|
||||
? _value.activeLibraryId
|
||||
: activeLibraryId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$AudiobookShelfServerCopyWith<$Res>? get activeServer {
|
||||
if (_value.activeServer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $AudiobookShelfServerCopyWith<$Res>(_value.activeServer!, (value) {
|
||||
return _then(_value.copyWith(activeServer: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$AuthenticatedUserCopyWith<$Res>? get activeUser {
|
||||
if (_value.activeUser == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $AuthenticatedUserCopyWith<$Res>(_value.activeUser!, (value) {
|
||||
return _then(_value.copyWith(activeUser: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ApiSettingsImplCopyWith<$Res>
|
||||
implements $ApiSettingsCopyWith<$Res> {
|
||||
factory _$$ApiSettingsImplCopyWith(
|
||||
_$ApiSettingsImpl value, $Res Function(_$ApiSettingsImpl) then) =
|
||||
__$$ApiSettingsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{AudiobookShelfServer? activeServer,
|
||||
AuthenticatedUser? activeUser,
|
||||
String? activeLibraryId});
|
||||
|
||||
@override
|
||||
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
|
||||
@override
|
||||
$AuthenticatedUserCopyWith<$Res>? get activeUser;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ApiSettingsImplCopyWithImpl<$Res>
|
||||
extends _$ApiSettingsCopyWithImpl<$Res, _$ApiSettingsImpl>
|
||||
implements _$$ApiSettingsImplCopyWith<$Res> {
|
||||
__$$ApiSettingsImplCopyWithImpl(
|
||||
_$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? activeServer = freezed,
|
||||
Object? activeUser = freezed,
|
||||
Object? activeLibraryId = freezed,
|
||||
}) {
|
||||
return _then(_$ApiSettingsImpl(
|
||||
activeServer: freezed == activeServer
|
||||
? _value.activeServer
|
||||
: activeServer // ignore: cast_nullable_to_non_nullable
|
||||
as AudiobookShelfServer?,
|
||||
activeUser: freezed == activeUser
|
||||
? _value.activeUser
|
||||
: activeUser // ignore: cast_nullable_to_non_nullable
|
||||
as AuthenticatedUser?,
|
||||
activeLibraryId: freezed == activeLibraryId
|
||||
? _value.activeLibraryId
|
||||
: activeLibraryId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$ApiSettingsImpl implements _ApiSettings {
|
||||
const _$ApiSettingsImpl(
|
||||
{this.activeServer, this.activeUser, this.activeLibraryId});
|
||||
|
||||
factory _$ApiSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$ApiSettingsImplFromJson(json);
|
||||
|
||||
@override
|
||||
final AudiobookShelfServer? activeServer;
|
||||
@override
|
||||
final AuthenticatedUser? activeUser;
|
||||
@override
|
||||
final String? activeLibraryId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ApiSettingsImpl &&
|
||||
(identical(other.activeServer, activeServer) ||
|
||||
other.activeServer == activeServer) &&
|
||||
(identical(other.activeUser, activeUser) ||
|
||||
other.activeUser == activeUser) &&
|
||||
(identical(other.activeLibraryId, activeLibraryId) ||
|
||||
other.activeLibraryId == activeLibraryId));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, activeServer, activeUser, activeLibraryId);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
|
||||
__$$ApiSettingsImplCopyWithImpl<_$ApiSettingsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$ApiSettingsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _ApiSettings implements ApiSettings {
|
||||
const factory _ApiSettings(
|
||||
{final AudiobookShelfServer? activeServer,
|
||||
final AuthenticatedUser? activeUser,
|
||||
final String? activeLibraryId}) = _$ApiSettingsImpl;
|
||||
|
||||
factory _ApiSettings.fromJson(Map<String, dynamic> json) =
|
||||
_$ApiSettingsImpl.fromJson;
|
||||
|
||||
@override
|
||||
AudiobookShelfServer? get activeServer;
|
||||
@override
|
||||
AuthenticatedUser? get activeUser;
|
||||
@override
|
||||
String? get activeLibraryId;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
27
lib/settings/models/api_settings.g.dart
Normal file
27
lib/settings/models/api_settings.g.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'api_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ApiSettingsImpl(
|
||||
activeServer: json['activeServer'] == null
|
||||
? null
|
||||
: AudiobookShelfServer.fromJson(
|
||||
json['activeServer'] as Map<String, dynamic>),
|
||||
activeUser: json['activeUser'] == null
|
||||
? null
|
||||
: AuthenticatedUser.fromJson(
|
||||
json['activeUser'] as Map<String, dynamic>),
|
||||
activeLibraryId: json['activeLibraryId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'activeServer': instance.activeServer,
|
||||
'activeUser': instance.activeUser,
|
||||
'activeLibraryId': instance.activeLibraryId,
|
||||
};
|
||||
19
lib/settings/models/app_settings.dart
Normal file
19
lib/settings/models/app_settings.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// a freezed class to store the settings of the app
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'app_settings.freezed.dart';
|
||||
part 'app_settings.g.dart';
|
||||
|
||||
/// stores the settings of the app
|
||||
///
|
||||
/// only the visual settings are stored here
|
||||
@freezed
|
||||
class AppSettings with _$AppSettings {
|
||||
const factory AppSettings({
|
||||
@Default(true) bool isDarkMode,
|
||||
}) = _AppSettings;
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppSettingsFromJson(json);
|
||||
}
|
||||
153
lib/settings/models/app_settings.freezed.dart
Normal file
153
lib/settings/models/app_settings.freezed.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'app_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) {
|
||||
return _AppSettings.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AppSettings {
|
||||
bool get isDarkMode => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$AppSettingsCopyWith<AppSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AppSettingsCopyWith<$Res> {
|
||||
factory $AppSettingsCopyWith(
|
||||
AppSettings value, $Res Function(AppSettings) then) =
|
||||
_$AppSettingsCopyWithImpl<$Res, AppSettings>;
|
||||
@useResult
|
||||
$Res call({bool isDarkMode});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||
implements $AppSettingsCopyWith<$Res> {
|
||||
_$AppSettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? isDarkMode = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
isDarkMode: null == isDarkMode
|
||||
? _value.isDarkMode
|
||||
: isDarkMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$AppSettingsImplCopyWith<$Res>
|
||||
implements $AppSettingsCopyWith<$Res> {
|
||||
factory _$$AppSettingsImplCopyWith(
|
||||
_$AppSettingsImpl value, $Res Function(_$AppSettingsImpl) then) =
|
||||
__$$AppSettingsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({bool isDarkMode});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$AppSettingsImplCopyWithImpl<$Res>
|
||||
extends _$AppSettingsCopyWithImpl<$Res, _$AppSettingsImpl>
|
||||
implements _$$AppSettingsImplCopyWith<$Res> {
|
||||
__$$AppSettingsImplCopyWithImpl(
|
||||
_$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? isDarkMode = null,
|
||||
}) {
|
||||
return _then(_$AppSettingsImpl(
|
||||
isDarkMode: null == isDarkMode
|
||||
? _value.isDarkMode
|
||||
: isDarkMode // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$AppSettingsImpl implements _AppSettings {
|
||||
const _$AppSettingsImpl({this.isDarkMode = true});
|
||||
|
||||
factory _$AppSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$AppSettingsImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isDarkMode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(isDarkMode: $isDarkMode)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$AppSettingsImpl &&
|
||||
(identical(other.isDarkMode, isDarkMode) ||
|
||||
other.isDarkMode == isDarkMode));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, isDarkMode);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
|
||||
__$$AppSettingsImplCopyWithImpl<_$AppSettingsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$AppSettingsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _AppSettings implements AppSettings {
|
||||
const factory _AppSettings({final bool isDarkMode}) = _$AppSettingsImpl;
|
||||
|
||||
factory _AppSettings.fromJson(Map<String, dynamic> json) =
|
||||
_$AppSettingsImpl.fromJson;
|
||||
|
||||
@override
|
||||
bool get isDarkMode;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
17
lib/settings/models/app_settings.g.dart
Normal file
17
lib/settings/models/app_settings.g.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$AppSettingsImpl _$$AppSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||
_$AppSettingsImpl(
|
||||
isDarkMode: json['isDarkMode'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'isDarkMode': instance.isDarkMode,
|
||||
};
|
||||
18
lib/settings/models/audiobookshelf_server.dart
Normal file
18
lib/settings/models/audiobookshelf_server.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'audiobookshelf_server.freezed.dart';
|
||||
part 'audiobookshelf_server.g.dart';
|
||||
|
||||
typedef AudiobookShelfUri = Uri;
|
||||
|
||||
/// Represents a audiobookshelf server
|
||||
@freezed
|
||||
class AudiobookShelfServer with _$AudiobookShelfServer {
|
||||
const factory AudiobookShelfServer({
|
||||
required AudiobookShelfUri serverUrl,
|
||||
// String? serverName,
|
||||
}) = _AudiobookShelfServer;
|
||||
|
||||
factory AudiobookShelfServer.fromJson(Map<String, dynamic> json) =>
|
||||
_$AudiobookShelfServerFromJson(json);
|
||||
}
|
||||
156
lib/settings/models/audiobookshelf_server.freezed.dart
Normal file
156
lib/settings/models/audiobookshelf_server.freezed.dart
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'audiobookshelf_server.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
AudiobookShelfServer _$AudiobookShelfServerFromJson(Map<String, dynamic> json) {
|
||||
return _AudiobookShelfServer.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AudiobookShelfServer {
|
||||
Uri get serverUrl => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AudiobookShelfServerCopyWith<$Res> {
|
||||
factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value,
|
||||
$Res Function(AudiobookShelfServer) then) =
|
||||
_$AudiobookShelfServerCopyWithImpl<$Res, AudiobookShelfServer>;
|
||||
@useResult
|
||||
$Res call({Uri serverUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AudiobookShelfServerCopyWithImpl<$Res,
|
||||
$Val extends AudiobookShelfServer>
|
||||
implements $AudiobookShelfServerCopyWith<$Res> {
|
||||
_$AudiobookShelfServerCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serverUrl = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
serverUrl: null == serverUrl
|
||||
? _value.serverUrl
|
||||
: serverUrl // ignore: cast_nullable_to_non_nullable
|
||||
as Uri,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$AudiobookShelfServerImplCopyWith<$Res>
|
||||
implements $AudiobookShelfServerCopyWith<$Res> {
|
||||
factory _$$AudiobookShelfServerImplCopyWith(_$AudiobookShelfServerImpl value,
|
||||
$Res Function(_$AudiobookShelfServerImpl) then) =
|
||||
__$$AudiobookShelfServerImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Uri serverUrl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$AudiobookShelfServerImplCopyWithImpl<$Res>
|
||||
extends _$AudiobookShelfServerCopyWithImpl<$Res, _$AudiobookShelfServerImpl>
|
||||
implements _$$AudiobookShelfServerImplCopyWith<$Res> {
|
||||
__$$AudiobookShelfServerImplCopyWithImpl(_$AudiobookShelfServerImpl _value,
|
||||
$Res Function(_$AudiobookShelfServerImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? serverUrl = null,
|
||||
}) {
|
||||
return _then(_$AudiobookShelfServerImpl(
|
||||
serverUrl: null == serverUrl
|
||||
? _value.serverUrl
|
||||
: serverUrl // ignore: cast_nullable_to_non_nullable
|
||||
as Uri,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$AudiobookShelfServerImpl implements _AudiobookShelfServer {
|
||||
const _$AudiobookShelfServerImpl({required this.serverUrl});
|
||||
|
||||
factory _$AudiobookShelfServerImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$AudiobookShelfServerImplFromJson(json);
|
||||
|
||||
@override
|
||||
final Uri serverUrl;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AudiobookShelfServer(serverUrl: $serverUrl)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$AudiobookShelfServerImpl &&
|
||||
(identical(other.serverUrl, serverUrl) ||
|
||||
other.serverUrl == serverUrl));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, serverUrl);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
|
||||
get copyWith =>
|
||||
__$$AudiobookShelfServerImplCopyWithImpl<_$AudiobookShelfServerImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$AudiobookShelfServerImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _AudiobookShelfServer implements AudiobookShelfServer {
|
||||
const factory _AudiobookShelfServer({required final Uri serverUrl}) =
|
||||
_$AudiobookShelfServerImpl;
|
||||
|
||||
factory _AudiobookShelfServer.fromJson(Map<String, dynamic> json) =
|
||||
_$AudiobookShelfServerImpl.fromJson;
|
||||
|
||||
@override
|
||||
Uri get serverUrl;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
19
lib/settings/models/audiobookshelf_server.g.dart
Normal file
19
lib/settings/models/audiobookshelf_server.g.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'audiobookshelf_server.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$AudiobookShelfServerImpl(
|
||||
serverUrl: Uri.parse(json['serverUrl'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AudiobookShelfServerImplToJson(
|
||||
_$AudiobookShelfServerImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'serverUrl': instance.serverUrl.toString(),
|
||||
};
|
||||
20
lib/settings/models/authenticated_user.dart
Normal file
20
lib/settings/models/authenticated_user.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||
|
||||
part 'authenticated_user.freezed.dart';
|
||||
part 'authenticated_user.g.dart';
|
||||
|
||||
/// authenticated user with server and credentials
|
||||
@freezed
|
||||
class AuthenticatedUser with _$AuthenticatedUser {
|
||||
const factory AuthenticatedUser({
|
||||
required AudiobookShelfServer server,
|
||||
required String authToken,
|
||||
String? id,
|
||||
String? username,
|
||||
String? password,
|
||||
}) = _AuthenticatedUser;
|
||||
|
||||
factory AuthenticatedUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthenticatedUserFromJson(json);
|
||||
}
|
||||
253
lib/settings/models/authenticated_user.freezed.dart
Normal file
253
lib/settings/models/authenticated_user.freezed.dart
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'authenticated_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
AuthenticatedUser _$AuthenticatedUserFromJson(Map<String, dynamic> json) {
|
||||
return _AuthenticatedUser.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AuthenticatedUser {
|
||||
AudiobookShelfServer get server => throw _privateConstructorUsedError;
|
||||
String get authToken => throw _privateConstructorUsedError;
|
||||
String? get id => throw _privateConstructorUsedError;
|
||||
String? get username => throw _privateConstructorUsedError;
|
||||
String? get password => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AuthenticatedUserCopyWith<$Res> {
|
||||
factory $AuthenticatedUserCopyWith(
|
||||
AuthenticatedUser value, $Res Function(AuthenticatedUser) then) =
|
||||
_$AuthenticatedUserCopyWithImpl<$Res, AuthenticatedUser>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{AudiobookShelfServer server,
|
||||
String authToken,
|
||||
String? id,
|
||||
String? username,
|
||||
String? password});
|
||||
|
||||
$AudiobookShelfServerCopyWith<$Res> get server;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
|
||||
implements $AuthenticatedUserCopyWith<$Res> {
|
||||
_$AuthenticatedUserCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? server = null,
|
||||
Object? authToken = null,
|
||||
Object? id = freezed,
|
||||
Object? username = freezed,
|
||||
Object? password = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
server: null == server
|
||||
? _value.server
|
||||
: server // ignore: cast_nullable_to_non_nullable
|
||||
as AudiobookShelfServer,
|
||||
authToken: null == authToken
|
||||
? _value.authToken
|
||||
: authToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
username: freezed == username
|
||||
? _value.username
|
||||
: username // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
password: freezed == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$AudiobookShelfServerCopyWith<$Res> get server {
|
||||
return $AudiobookShelfServerCopyWith<$Res>(_value.server, (value) {
|
||||
return _then(_value.copyWith(server: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$AuthenticatedUserImplCopyWith<$Res>
|
||||
implements $AuthenticatedUserCopyWith<$Res> {
|
||||
factory _$$AuthenticatedUserImplCopyWith(_$AuthenticatedUserImpl value,
|
||||
$Res Function(_$AuthenticatedUserImpl) then) =
|
||||
__$$AuthenticatedUserImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{AudiobookShelfServer server,
|
||||
String authToken,
|
||||
String? id,
|
||||
String? username,
|
||||
String? password});
|
||||
|
||||
@override
|
||||
$AudiobookShelfServerCopyWith<$Res> get server;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$AuthenticatedUserImplCopyWithImpl<$Res>
|
||||
extends _$AuthenticatedUserCopyWithImpl<$Res, _$AuthenticatedUserImpl>
|
||||
implements _$$AuthenticatedUserImplCopyWith<$Res> {
|
||||
__$$AuthenticatedUserImplCopyWithImpl(_$AuthenticatedUserImpl _value,
|
||||
$Res Function(_$AuthenticatedUserImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? server = null,
|
||||
Object? authToken = null,
|
||||
Object? id = freezed,
|
||||
Object? username = freezed,
|
||||
Object? password = freezed,
|
||||
}) {
|
||||
return _then(_$AuthenticatedUserImpl(
|
||||
server: null == server
|
||||
? _value.server
|
||||
: server // ignore: cast_nullable_to_non_nullable
|
||||
as AudiobookShelfServer,
|
||||
authToken: null == authToken
|
||||
? _value.authToken
|
||||
: authToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
username: freezed == username
|
||||
? _value.username
|
||||
: username // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
password: freezed == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$AuthenticatedUserImpl implements _AuthenticatedUser {
|
||||
const _$AuthenticatedUserImpl(
|
||||
{required this.server,
|
||||
required this.authToken,
|
||||
this.id,
|
||||
this.username,
|
||||
this.password});
|
||||
|
||||
factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$AuthenticatedUserImplFromJson(json);
|
||||
|
||||
@override
|
||||
final AudiobookShelfServer server;
|
||||
@override
|
||||
final String authToken;
|
||||
@override
|
||||
final String? id;
|
||||
@override
|
||||
final String? username;
|
||||
@override
|
||||
final String? password;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$AuthenticatedUserImpl &&
|
||||
(identical(other.server, server) || other.server == server) &&
|
||||
(identical(other.authToken, authToken) ||
|
||||
other.authToken == authToken) &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.username, username) ||
|
||||
other.username == username) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, server, authToken, id, username, password);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
|
||||
__$$AuthenticatedUserImplCopyWithImpl<_$AuthenticatedUserImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$AuthenticatedUserImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _AuthenticatedUser implements AuthenticatedUser {
|
||||
const factory _AuthenticatedUser(
|
||||
{required final AudiobookShelfServer server,
|
||||
required final String authToken,
|
||||
final String? id,
|
||||
final String? username,
|
||||
final String? password}) = _$AuthenticatedUserImpl;
|
||||
|
||||
factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) =
|
||||
_$AuthenticatedUserImpl.fromJson;
|
||||
|
||||
@override
|
||||
AudiobookShelfServer get server;
|
||||
@override
|
||||
String get authToken;
|
||||
@override
|
||||
String? get id;
|
||||
@override
|
||||
String? get username;
|
||||
@override
|
||||
String? get password;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
28
lib/settings/models/authenticated_user.g.dart
Normal file
28
lib/settings/models/authenticated_user.g.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'authenticated_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$AuthenticatedUserImpl(
|
||||
server:
|
||||
AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>),
|
||||
authToken: json['authToken'] as String,
|
||||
id: json['id'] as String?,
|
||||
username: json['username'] as String?,
|
||||
password: json['password'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AuthenticatedUserImplToJson(
|
||||
_$AuthenticatedUserImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'server': instance.server,
|
||||
'authToken': instance.authToken,
|
||||
'id': instance.id,
|
||||
'username': instance.username,
|
||||
'password': instance.password,
|
||||
};
|
||||
4
lib/settings/models/models.dart
Normal file
4
lib/settings/models/models.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export 'api_settings.dart';
|
||||
export 'app_settings.dart';
|
||||
export 'audiobookshelf_server.dart';
|
||||
export 'authenticated_user.dart';
|
||||
2
lib/settings/settings.dart
Normal file
2
lib/settings/settings.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'app_settings_provider.dart';
|
||||
export 'constants.dart';
|
||||
8
lib/theme/dark.dart
Normal file
8
lib/theme/dark.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
final ThemeData darkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.dark(
|
||||
background: Colors.grey[900]!,
|
||||
),
|
||||
);
|
||||
8
lib/theme/light.dart
Normal file
8
lib/theme/light.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
final ThemeData lightTheme = ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.light(
|
||||
background: Colors.grey[200]!,
|
||||
),
|
||||
);
|
||||
2
lib/theme/theme.dart
Normal file
2
lib/theme/theme.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'dark.dart';
|
||||
export 'light.dart';
|
||||
107
lib/widgets/add_new_server.dart
Normal file
107
lib/widgets/add_new_server.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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';
|
||||
|
||||
class AddNewServer extends HookConsumerWidget {
|
||||
const AddNewServer({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.onPressed,
|
||||
this.readOnly = false,
|
||||
this.allowEmpty = false,
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// the function to call when the button is pressed
|
||||
final void Function()? onPressed;
|
||||
|
||||
/// if this field is read only
|
||||
final bool readOnly;
|
||||
|
||||
/// the server URI can be empty
|
||||
final bool allowEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final myController = controller ?? useTextEditingController();
|
||||
var newServerURI = useValueListenable(myController);
|
||||
final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text));
|
||||
bool isServerAliveValue = isServerAlive.when(
|
||||
data: (value) => value,
|
||||
loading: () => false,
|
||||
error: (error, _) => false,
|
||||
);
|
||||
|
||||
return TextFormField(
|
||||
readOnly: readOnly,
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Server URI',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: 'https://',
|
||||
prefixIcon: Tooltip(
|
||||
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.onBackground,
|
||||
)
|
||||
: 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
|
||||
suffixIcon: onPressed == null
|
||||
? null
|
||||
: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: IconButton.filled(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Add new server',
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
focusColor: Theme.of(context).colorScheme.onBackground,
|
||||
|
||||
// should be enabled when
|
||||
onPressed: !readOnly &&
|
||||
(isServerAliveValue ||
|
||||
(allowEmpty && newServerURI.text.isEmpty))
|
||||
? onPressed
|
||||
: null, // disable button if server is not alive
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// add to add to existing servers
|
||||
}
|
||||
}
|
||||
49
lib/widgets/drawer.dart
Normal file
49
lib/widgets/drawer.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:whispering_pages/pages/app_settings.dart';
|
||||
import 'package:whispering_pages/pages/server_manager.dart';
|
||||
|
||||
|
||||
class MyDrawer extends StatelessWidget {
|
||||
const MyDrawer({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
child: Text(
|
||||
'Whispering Pages',
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('server Settings'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ServerManagerPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('App Settings'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppSettingsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/widgets/shelves/author_shelf.dart
Normal file
82
lib/widgets/shelves/author_shelf.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/api/image_provider.dart';
|
||||
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
|
||||
|
||||
/// A shelf that displays Authors on the home page
|
||||
class AuthorHomeShelf extends HookConsumerWidget {
|
||||
const AuthorHomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final AuthorShelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleHomeShelf(
|
||||
title: title,
|
||||
children: shelf.entities
|
||||
.map(
|
||||
(item) => AuthorOnShelf(item: item),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a widget to display a item on the shelf
|
||||
class AuthorOnShelf extends HookConsumerWidget {
|
||||
const AuthorOnShelf({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
final Author item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final author = AuthorMinified.fromJson(item.toJson());
|
||||
// final coverImage = ref.watch(coverImageProvider(item));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
constraints: const BoxConstraints(maxWidth: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 50),
|
||||
// child: coverImage.when(
|
||||
// data: (image) {
|
||||
// return Image.memory(image, fit: BoxFit.cover);
|
||||
// },
|
||||
// loading: () {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// },
|
||||
// error: (error, stack) {
|
||||
// return const Icon(Icons.error);
|
||||
// },
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
child: Text(
|
||||
author.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/widgets/shelves/book_shelf.dart
Normal file
118
lib/widgets/shelves/book_shelf.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:auto_scroll_text/auto_scroll_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/api/image_provider.dart';
|
||||
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
|
||||
|
||||
/// A shelf that displays books on the home page
|
||||
class BookHomeShelf extends HookConsumerWidget {
|
||||
const BookHomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final LibraryItemShelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleHomeShelf(
|
||||
title: title,
|
||||
children: shelf.entities
|
||||
.map(
|
||||
(item) => switch (item.mediaType) {
|
||||
MediaType.book => BookOnShelf(
|
||||
item: item,
|
||||
key: ValueKey(shelf.id + item.id),
|
||||
),
|
||||
_ => Container(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a widget to display a item on the shelf
|
||||
class BookOnShelf extends HookConsumerWidget {
|
||||
const BookOnShelf({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
final LibraryItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final book = BookMinified.fromJson(item.media.toJson());
|
||||
final metadata = BookMetadataMinified.fromJson(book.metadata.toJson());
|
||||
final coverImage = ref.watch(coverImageProvider(item));
|
||||
const coverSize = 150.0;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
constraints: const BoxConstraints(maxWidth: coverSize),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: coverSize),
|
||||
color: Colors.grey[800],
|
||||
child: coverImage.when(
|
||||
data: (image) {
|
||||
if (image.isEmpty) {
|
||||
return const Icon(Icons.error);
|
||||
}
|
||||
return Image.memory(
|
||||
image,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth:
|
||||
(coverSize * MediaQuery.of(context).devicePixelRatio)
|
||||
.round(),
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
error: (error, stack) {
|
||||
return const Icon(Icons.error);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AutoScrollText(
|
||||
metadata.title ?? '',
|
||||
mode: AutoScrollTextMode.bouncing,
|
||||
curve: Curves.easeInOut,
|
||||
velocity: const Velocity(pixelsPerSecond: Offset(15, 0)),
|
||||
delayBefore: const Duration(seconds: 2),
|
||||
pauseBetween: const Duration(seconds: 2),
|
||||
numberOfReps: 15,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
metadata.authorName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/widgets/shelves/home_shelf.dart
Normal file
66
lib/widgets/shelves/home_shelf.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/widgets/shelves/author_shelf.dart';
|
||||
import 'package:whispering_pages/widgets/shelves/book_shelf.dart';
|
||||
|
||||
/// A shelf that displays books/authors/series on the home page
|
||||
///
|
||||
/// this will build the appropriate shelf based on the type of the shelf
|
||||
class HomeShelf extends HookConsumerWidget {
|
||||
const HomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Shelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return switch (shelf.type) {
|
||||
ShelfType.book => BookHomeShelf(
|
||||
title: title,
|
||||
shelf: LibraryItemShelf.fromJson(shelf.toJson()),
|
||||
),
|
||||
ShelfType.authors => AuthorHomeShelf(
|
||||
title: title,
|
||||
shelf: AuthorShelf.fromJson(shelf.toJson()),
|
||||
),
|
||||
_ => Container(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A shelf that displays books on the home page
|
||||
class SimpleHomeShelf extends HookConsumerWidget {
|
||||
const SimpleHomeShelf({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title,
|
||||
const SizedBox(height: 16),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/widgets/user_login.dart
Normal file
119
lib/widgets/user_login.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:whispering_pages/hacks/fix_autofill_losing_focus.dart';
|
||||
|
||||
class UserLogin extends HookConsumerWidget {
|
||||
UserLogin({
|
||||
super.key,
|
||||
this.usernameController,
|
||||
this.passwordController,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
TextEditingController? usernameController;
|
||||
TextEditingController? passwordController;
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
usernameController ??= useTextEditingController();
|
||||
passwordController ??= useTextEditingController();
|
||||
final isPasswordVisibleAnimationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
var isPasswordVisible = useState(false);
|
||||
|
||||
// forward animation when the password visibility changes
|
||||
useEffect(
|
||||
() {
|
||||
if (isPasswordVisible.value) {
|
||||
isPasswordVisibleAnimationController.forward();
|
||||
} else {
|
||||
isPasswordVisibleAnimationController.reverse();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[isPasswordVisible.value],
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: InactiveFocusScopeObserver(
|
||||
child: AutofillGroup(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: usernameController,
|
||||
autofocus: true,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: !isPasswordVisible.value,
|
||||
onFieldSubmitted: onPressed != null
|
||||
? (_) {
|
||||
onPressed!();
|
||||
}
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/Animation - 1714930099660.json',
|
||||
controller: isPasswordVisibleAnimationController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
maxHeight: 45,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue