mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-01-17 15:49:32 +00:00
chore: run dart format
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
This commit is contained in:
parent
a520136e01
commit
e23c0b6c5f
84 changed files with 1565 additions and 1945 deletions
|
|
@ -16,10 +16,8 @@ import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
part 'api_provider.g.dart';
|
part 'api_provider.g.dart';
|
||||||
|
|
||||||
// TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718
|
// TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718
|
||||||
typedef ResponseErrorHandler = void Function(
|
typedef ResponseErrorHandler =
|
||||||
Response response, [
|
void Function(Response response, [Object? error]);
|
||||||
Object? error,
|
|
||||||
]);
|
|
||||||
|
|
||||||
final _logger = Logger('api_provider');
|
final _logger = Logger('api_provider');
|
||||||
|
|
||||||
|
|
@ -39,9 +37,7 @@ AudiobookshelfApi audiobookshelfApi(Ref ref, Uri? baseUrl) {
|
||||||
// try to get the base url from app settings
|
// try to get the base url from app settings
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
||||||
return AudiobookshelfApi(
|
return AudiobookshelfApi(baseUrl: makeBaseUrl(baseUrl.toString()));
|
||||||
baseUrl: makeBaseUrl(baseUrl.toString()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the api instance for the authenticated user
|
/// get the api instance for the authenticated user
|
||||||
|
|
@ -68,9 +64,9 @@ FutureOr<bool> isServerAlive(Ref ref, String address) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await AudiobookshelfApi(baseUrl: makeBaseUrl(address))
|
return await AudiobookshelfApi(
|
||||||
.server
|
baseUrl: makeBaseUrl(address),
|
||||||
.ping() ??
|
).server.ping() ??
|
||||||
false;
|
false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -86,8 +82,9 @@ FutureOr<ServerStatusResponse?> serverStatus(
|
||||||
]) async {
|
]) async {
|
||||||
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
|
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
|
||||||
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
|
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
|
||||||
final res =
|
final res = await api.server.status(
|
||||||
await api.server.status(responseErrorHandler: responseErrorHandler);
|
responseErrorHandler: responseErrorHandler,
|
||||||
|
);
|
||||||
_logger.fine('server status: $res');
|
_logger.fine('server status: $res');
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +110,9 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
yield [];
|
yield [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
|
.read(apiSettingsProvider.notifier)
|
||||||
|
.updateState(
|
||||||
apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
|
apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
|
||||||
);
|
);
|
||||||
yield [];
|
yield [];
|
||||||
|
|
@ -122,9 +121,8 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
// try to find in cache
|
// try to find in cache
|
||||||
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
||||||
final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
|
final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
|
||||||
final cachedRes = await apiResponseCacheManager.getFileFromMemory(
|
final cachedRes =
|
||||||
key,
|
await apiResponseCacheManager.getFileFromMemory(key) ??
|
||||||
) ??
|
|
||||||
await apiResponseCacheManager.getFileFromCache(key);
|
await apiResponseCacheManager.getFileFromCache(key);
|
||||||
if (cachedRes != null) {
|
if (cachedRes != null) {
|
||||||
_logger.fine('reading from cache: $cachedRes for key: $key');
|
_logger.fine('reading from cache: $cachedRes for key: $key');
|
||||||
|
|
@ -143,8 +141,9 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
|
|
||||||
// ! exaggerated delay
|
// ! exaggerated delay
|
||||||
// await Future.delayed(const Duration(seconds: 2));
|
// await Future.delayed(const Duration(seconds: 2));
|
||||||
final res = await api.libraries
|
final res = await api.libraries.getPersonalized(
|
||||||
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
|
libraryId: apiSettings.activeLibraryId!,
|
||||||
|
);
|
||||||
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
|
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
|
||||||
// save to cache
|
// save to cache
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
|
|
@ -172,9 +171,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
|
|
||||||
/// fetch continue listening audiobooks
|
/// fetch continue listening audiobooks
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
FutureOr<GetUserSessionsResponse> fetchContinueListening(Ref ref) async {
|
||||||
Ref ref,
|
|
||||||
) async {
|
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final res = await api.me.getSessions();
|
final res = await api.me.getSessions();
|
||||||
// debugPrint(
|
// debugPrint(
|
||||||
|
|
@ -184,9 +181,7 @@ FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<User> me(
|
FutureOr<User> me(Ref ref) async {
|
||||||
Ref ref,
|
|
||||||
) async {
|
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final errorResponseHandler = ErrorResponseHandler();
|
final errorResponseHandler = ErrorResponseHandler();
|
||||||
final res = await api.me.getUser(
|
final res = await api.me.getUser(
|
||||||
|
|
@ -202,10 +197,7 @@ FutureOr<User> me(
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<LoginResponse?> login(
|
FutureOr<LoginResponse?> login(Ref ref, {AuthenticatedUser? user}) async {
|
||||||
Ref ref, {
|
|
||||||
AuthenticatedUser? user,
|
|
||||||
}) async {
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
// try to get the user from settings
|
// try to get the user from settings
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ class AuthenticatedUsers extends _$AuthenticatedUsers {
|
||||||
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
||||||
if (_box.isNotEmpty) {
|
if (_box.isNotEmpty) {
|
||||||
final foundData = _box.getRange(0, _box.length);
|
final foundData = _box.getRange(0, _box.length);
|
||||||
_logger.fine(
|
_logger.fine('found users in box: ${foundData.obfuscate()}');
|
||||||
'found users in box: ${foundData.obfuscate()}',
|
|
||||||
);
|
|
||||||
return foundData.toSet();
|
return foundData.toSet();
|
||||||
} else {
|
} else {
|
||||||
_logger.fine('no settings found in box');
|
_logger.fine('no settings found in box');
|
||||||
|
|
@ -59,11 +57,9 @@ class AuthenticatedUsers extends _$AuthenticatedUsers {
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
if (setActive) {
|
if (setActive) {
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
apiSettings.copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeUser: user,
|
.updateState(apiSettings.copyWith(activeUser: user));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,11 +82,9 @@ class AuthenticatedUsers extends _$AuthenticatedUsers {
|
||||||
// replace the active user with the first user in the list
|
// replace the active user with the first user in the list
|
||||||
// or null if there are no users left
|
// or null if there are no users left
|
||||||
final newActiveUser = state.isNotEmpty ? state.first : null;
|
final newActiveUser = state.isNotEmpty ? state.first : null;
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
apiSettings.copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeUser: newActiveUser,
|
.updateState(apiSettings.copyWith(activeUser: newActiveUser));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ class CoverImage extends _$CoverImage {
|
||||||
// await Future.delayed(const Duration(seconds: 2));
|
// await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
// try to get the image from the cache
|
// try to get the image from the cache
|
||||||
final file = await imageCacheManager.getFileFromMemory(itemId) ??
|
final file =
|
||||||
|
await imageCacheManager.getFileFromMemory(itemId) ??
|
||||||
await imageCacheManager.getFileFromCache(itemId);
|
await imageCacheManager.getFileFromCache(itemId);
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
|
|
@ -44,9 +45,7 @@ class CoverImage extends _$CoverImage {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
_logger.fine(
|
_logger.fine('cover image stale for $itemId, fetching from the server');
|
||||||
'cover image stale for $itemId, fetching from the server',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_logger.fine('cover image not found in cache for $itemId');
|
_logger.fine('cover image not found in cache for $itemId');
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ class LibraryItem extends _$LibraryItem {
|
||||||
|
|
||||||
// look for the item in the cache
|
// look for the item in the cache
|
||||||
final key = CacheKey.libraryItem(id);
|
final key = CacheKey.libraryItem(id);
|
||||||
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
|
final cachedFile =
|
||||||
|
await apiResponseCacheManager.getFileFromMemory(key) ??
|
||||||
await apiResponseCacheManager.getFileFromCache(key);
|
await apiResponseCacheManager.getFileFromCache(key);
|
||||||
if (cachedFile != null) {
|
if (cachedFile != null) {
|
||||||
_logger.fine(
|
_logger.fine(
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,9 @@ Future<Library?> library(Ref ref, String id) async {
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<Library?> currentLibrary(Ref ref) async {
|
Future<Library?> currentLibrary(Ref ref) async {
|
||||||
final libraryId =
|
final libraryId = ref.watch(
|
||||||
ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId));
|
apiSettingsProvider.select((s) => s.activeLibraryId),
|
||||||
|
);
|
||||||
if (libraryId == null) {
|
if (libraryId == null) {
|
||||||
_logger.warning('No active library id found');
|
_logger.warning('No active library id found');
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,9 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
// remove the server from the active server
|
// remove the server from the active server
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
if (apiSettings.activeServer == server) {
|
if (apiSettings.activeServer == server) {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
apiSettings.copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeServer: null,
|
.updateState(apiSettings.copyWith(activeServer: null));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// remove the users of this server
|
// remove the users of this server
|
||||||
if (removeUsers) {
|
if (removeUsers) {
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,17 @@ class AvailableHiveBoxes {
|
||||||
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
|
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
|
||||||
|
|
||||||
/// stores the a list of [AudiobookShelfServer]
|
/// stores the a list of [AudiobookShelfServer]
|
||||||
static final serverBox =
|
static final serverBox = Hive.box<AudiobookShelfServer>(
|
||||||
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer');
|
name: 'audiobookShelfServer',
|
||||||
|
);
|
||||||
|
|
||||||
/// stores the a list of [AuthenticatedUser]
|
/// stores the a list of [AuthenticatedUser]
|
||||||
static final authenticatedUserBox =
|
static final authenticatedUserBox = Hive.box<AuthenticatedUser>(
|
||||||
Hive.box<AuthenticatedUser>(name: 'authenticatedUser');
|
name: 'authenticatedUser',
|
||||||
|
);
|
||||||
|
|
||||||
/// stores the a list of [BookSettings]
|
/// stores the a list of [BookSettings]
|
||||||
static final individualBookSettingsBox =
|
static final individualBookSettingsBox = Hive.box<BookSettings>(
|
||||||
Hive.box<BookSettings>(name: 'bookSettings');
|
name: 'bookSettings',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,7 @@ Future initStorage() async {
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
// use vaani as the directory for hive
|
// use vaani as the directory for hive
|
||||||
final storageDir = Directory(
|
final storageDir = Directory(p.join(dir.path, AppMetadata.appNameLowerCase));
|
||||||
p.join(
|
|
||||||
dir.path,
|
|
||||||
AppMetadata.appNameLowerCase,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await storageDir.create(recursive: true);
|
await storageDir.create(recursive: true);
|
||||||
|
|
||||||
Hive.defaultDirectory = storageDir.path;
|
Hive.defaultDirectory = storageDir.path;
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,13 @@ class AudiobookDownloadManager {
|
||||||
|
|
||||||
late StreamSubscription<TaskUpdate> _updatesSubscription;
|
late StreamSubscription<TaskUpdate> _updatesSubscription;
|
||||||
|
|
||||||
Future<void> queueAudioBookDownload(
|
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
|
||||||
LibraryItemExpanded item,
|
|
||||||
) async {
|
|
||||||
_logger.info('queuing download for item: ${item.id}');
|
_logger.info('queuing download for item: ${item.id}');
|
||||||
// create a download task for each file in the item
|
// create a download task for each file in the item
|
||||||
final directory = await getApplicationSupportDirectory();
|
final directory = await getApplicationSupportDirectory();
|
||||||
for (final file in item.libraryFiles) {
|
for (final file in item.libraryFiles) {
|
||||||
// check if the file is already downloaded
|
// check if the file is already downloaded
|
||||||
if (isFileDownloaded(
|
if (isFileDownloaded(constructFilePath(directory, item, file))) {
|
||||||
constructFilePath(directory, item, file),
|
|
||||||
)) {
|
|
||||||
_logger.info('file already downloaded: ${file.metadata.filename}');
|
_logger.info('file already downloaded: ${file.metadata.filename}');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +101,7 @@ class AudiobookDownloadManager {
|
||||||
Directory directory,
|
Directory directory,
|
||||||
LibraryItemExpanded item,
|
LibraryItemExpanded item,
|
||||||
LibraryFile file,
|
LibraryFile file,
|
||||||
) =>
|
) => '${directory.path}/${item.relPath}/${file.metadata.filename}';
|
||||||
'${directory.path}/${item.relPath}/${file.metadata.filename}';
|
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_updatesSubscription.cancel();
|
_updatesSubscription.cancel();
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,9 @@ class DownloadManager extends _$DownloadManager {
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> queueAudioBookDownload(
|
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
|
||||||
LibraryItemExpanded item,
|
|
||||||
) async {
|
|
||||||
_logger.fine('queueing download for ${item.id}');
|
_logger.fine('queueing download for ${item.id}');
|
||||||
await state.queueAudioBookDownload(
|
await state.queueAudioBookDownload(item);
|
||||||
item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
||||||
|
|
@ -83,58 +79,57 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
|
||||||
Future<double?> build(String id) async {
|
Future<double?> build(String id) async {
|
||||||
final item = await ref.watch(libraryItemProvider(id).future);
|
final item = await ref.watch(libraryItemProvider(id).future);
|
||||||
final manager = ref.read(downloadManagerProvider);
|
final manager = ref.read(downloadManagerProvider);
|
||||||
manager.taskUpdateStream.map((taskUpdate) {
|
manager.taskUpdateStream
|
||||||
if (taskUpdate is! TaskProgressUpdate) {
|
.map((taskUpdate) {
|
||||||
return null;
|
if (taskUpdate is! TaskProgressUpdate) {
|
||||||
}
|
return null;
|
||||||
if (taskUpdate.task.group == id) {
|
}
|
||||||
return taskUpdate;
|
if (taskUpdate.task.group == id) {
|
||||||
}
|
return taskUpdate;
|
||||||
}).listen((task) async {
|
}
|
||||||
if (task != null) {
|
})
|
||||||
final totalSize = item.totalSize;
|
.listen((task) async {
|
||||||
// if total size is 0, return 0
|
if (task != null) {
|
||||||
if (totalSize == 0) {
|
final totalSize = item.totalSize;
|
||||||
state = const AsyncValue.data(0.0);
|
// if total size is 0, return 0
|
||||||
return;
|
if (totalSize == 0) {
|
||||||
}
|
state = const AsyncValue.data(0.0);
|
||||||
final downloadedFiles = await manager.getDownloadedFilesMetadata(item);
|
return;
|
||||||
// calculate total size of downloaded files and total size of item, then divide
|
}
|
||||||
// to get percentage
|
final downloadedFiles = await manager.getDownloadedFilesMetadata(
|
||||||
final downloadedSize = downloadedFiles.fold<int>(
|
item,
|
||||||
0,
|
);
|
||||||
(previousValue, element) => previousValue + element.metadata.size,
|
// calculate total size of downloaded files and total size of item, then divide
|
||||||
);
|
// to get percentage
|
||||||
|
final downloadedSize = downloadedFiles.fold<int>(
|
||||||
|
0,
|
||||||
|
(previousValue, element) => previousValue + element.metadata.size,
|
||||||
|
);
|
||||||
|
|
||||||
final inProgressFileSize = task.progress * task.expectedFileSize;
|
final inProgressFileSize = task.progress * task.expectedFileSize;
|
||||||
final totalDownloadedSize = downloadedSize + inProgressFileSize;
|
final totalDownloadedSize = downloadedSize + inProgressFileSize;
|
||||||
final progress = totalDownloadedSize / totalSize;
|
final progress = totalDownloadedSize / totalSize;
|
||||||
// if current progress is more than calculated progress, do not update
|
// if current progress is more than calculated progress, do not update
|
||||||
if (progress < (state.value ?? 0.0)) {
|
if (progress < (state.value ?? 0.0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = AsyncValue.data(progress.clamp(0.0, 1.0));
|
state = AsyncValue.data(progress.clamp(0.0, 1.0));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<List<TaskRecord>> downloadHistory(
|
FutureOr<List<TaskRecord>> downloadHistory(Ref ref, {String? group}) async {
|
||||||
Ref ref, {
|
|
||||||
String? group,
|
|
||||||
}) async {
|
|
||||||
return await FileDownloader().database.allRecords(group: group);
|
return await FileDownloader().database.allRecords(group: group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class IsItemDownloaded extends _$IsItemDownloaded {
|
class IsItemDownloaded extends _$IsItemDownloaded {
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> build(
|
FutureOr<bool> build(LibraryItemExpanded item) {
|
||||||
LibraryItemExpanded item,
|
|
||||||
) {
|
|
||||||
final manager = ref.watch(downloadManagerProvider);
|
final manager = ref.watch(downloadManagerProvider);
|
||||||
return manager.isItemDownloaded(item);
|
return manager.isItemDownloaded(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ class DownloadsPage extends HookConsumerWidget {
|
||||||
final downloadHistory = ref.watch(downloadHistoryProvider());
|
final downloadHistory = ref.watch(downloadHistoryProvider());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Downloads')),
|
||||||
title: const Text('Downloads'),
|
|
||||||
),
|
|
||||||
body: Center(
|
body: Center(
|
||||||
// history of downloads
|
// history of downloads
|
||||||
child: downloadHistory.when(
|
child: downloadHistory.when(
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,14 @@ class ExplorePage extends HookConsumerWidget {
|
||||||
final settings = ref.watch(appSettingsProvider);
|
final settings = ref.watch(appSettingsProvider);
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Explore')),
|
||||||
title: const Text('Explore'),
|
|
||||||
),
|
|
||||||
body: const MySearchBar(),
|
body: const MySearchBar(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MySearchBar extends HookConsumerWidget {
|
class MySearchBar extends HookConsumerWidget {
|
||||||
const MySearchBar({
|
const MySearchBar({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -61,8 +57,11 @@ class MySearchBar extends HookConsumerWidget {
|
||||||
currentQuery = query;
|
currentQuery = query;
|
||||||
|
|
||||||
// In a real application, there should be some error handling here.
|
// In a real application, there should be some error handling here.
|
||||||
final options = await api.libraries
|
final options = await api.libraries.search(
|
||||||
.search(libraryId: settings.activeLibraryId!, query: query, limit: 3);
|
libraryId: settings.activeLibraryId!,
|
||||||
|
query: query,
|
||||||
|
limit: 3,
|
||||||
|
);
|
||||||
|
|
||||||
// If another search happened after this one, throw away these options.
|
// If another search happened after this one, throw away these options.
|
||||||
if (currentQuery != query) {
|
if (currentQuery != query) {
|
||||||
|
|
@ -97,11 +96,10 @@ class MySearchBar extends HookConsumerWidget {
|
||||||
// opacity: 0.5 for the hint text
|
// opacity: 0.5 for the hint text
|
||||||
hintStyle: WidgetStatePropertyAll(
|
hintStyle: WidgetStatePropertyAll(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
.withValues(alpha: 0.5),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
onTapOutside: (_) {
|
onTapOutside: (_) {
|
||||||
|
|
@ -120,12 +118,7 @@ class MySearchBar extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
viewOnSubmitted: (value) {
|
viewOnSubmitted: (value) {
|
||||||
context.pushNamed(
|
context.pushNamed(Routes.search.name, queryParameters: {'q': value});
|
||||||
Routes.search.name,
|
|
||||||
queryParameters: {
|
|
||||||
'q': value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
suggestionsBuilder: (context, controller) async {
|
suggestionsBuilder: (context, controller) async {
|
||||||
// check if the search controller is empty
|
// check if the search controller is empty
|
||||||
|
|
@ -191,14 +184,12 @@ List<Widget> buildBookSearchResult(
|
||||||
SearchResultMiniSection(
|
SearchResultMiniSection(
|
||||||
// title: 'Books',
|
// title: 'Books',
|
||||||
category: SearchResultCategory.books,
|
category: SearchResultCategory.books,
|
||||||
options: options.book.map(
|
options: options.book.map((result) {
|
||||||
(result) {
|
// convert result to a book object
|
||||||
// convert result to a book object
|
final book = result.libraryItem.media.asBookExpanded;
|
||||||
final book = result.libraryItem.media.asBookExpanded;
|
final metadata = book.metadata.asBookMetadataExpanded;
|
||||||
final metadata = book.metadata.asBookMetadataExpanded;
|
return BookSearchResultMini(book: book, metadata: metadata);
|
||||||
return BookSearchResultMini(book: book, metadata: metadata);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -207,11 +198,9 @@ List<Widget> buildBookSearchResult(
|
||||||
SearchResultMiniSection(
|
SearchResultMiniSection(
|
||||||
// title: 'Authors',
|
// title: 'Authors',
|
||||||
category: SearchResultCategory.authors,
|
category: SearchResultCategory.authors,
|
||||||
options: options.authors.map(
|
options: options.authors.map((result) {
|
||||||
(result) {
|
return ListTile(title: Text(result.name));
|
||||||
return ListTile(title: Text(result.name));
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -245,10 +234,7 @@ class BookSearchResultMini extends HookConsumerWidget {
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
child: image.when(
|
child: image.when(
|
||||||
data: (bytes) => Image.memory(
|
data: (bytes) => Image.memory(bytes, fit: BoxFit.cover),
|
||||||
bytes,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
loading: () => const BookCoverSkeleton(),
|
loading: () => const BookCoverSkeleton(),
|
||||||
error: (error, _) => const Icon(Icons.error),
|
error: (error, _) => const Icon(Icons.error),
|
||||||
),
|
),
|
||||||
|
|
@ -259,11 +245,7 @@ class BookSearchResultMini extends HookConsumerWidget {
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
metadata.authors
|
metadata.authors.map((author) => author.name).join(', '),
|
||||||
.map(
|
|
||||||
(author) => author.name,
|
|
||||||
)
|
|
||||||
.join(', '),
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// navigate to the book details page
|
// navigate to the book details page
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,7 @@ import 'package:vaani/features/explore/providers/search_result_provider.dart';
|
||||||
import 'package:vaani/features/explore/view/explore_page.dart';
|
import 'package:vaani/features/explore/view/explore_page.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
|
||||||
enum SearchResultCategory {
|
enum SearchResultCategory { books, authors, series, tags, narrators }
|
||||||
books,
|
|
||||||
authors,
|
|
||||||
series,
|
|
||||||
tags,
|
|
||||||
narrators,
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
const SearchResultPage({
|
const SearchResultPage({
|
||||||
|
|
@ -41,9 +35,7 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
body: results.when(
|
body: results.when(
|
||||||
data: (options) {
|
data: (options) {
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
return Container(
|
return Container(child: const Text('No data found'));
|
||||||
child: const Text('No data found'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (options is BookLibrarySearchResponse) {
|
if (options is BookLibrarySearchResponse) {
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
|
|
@ -51,18 +43,15 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
return switch (category!) {
|
return switch (category!) {
|
||||||
SearchResultCategory.books => ListView.builder(
|
SearchResultCategory.books => ListView.builder(
|
||||||
itemCount: options.book.length,
|
itemCount: options.book.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final book =
|
final book =
|
||||||
options.book[index].libraryItem.media.asBookExpanded;
|
options.book[index].libraryItem.media.asBookExpanded;
|
||||||
final metadata = book.metadata.asBookMetadataExpanded;
|
final metadata = book.metadata.asBookMetadataExpanded;
|
||||||
|
|
||||||
return BookSearchResultMini(
|
return BookSearchResultMini(book: book, metadata: metadata);
|
||||||
book: book,
|
},
|
||||||
metadata: metadata,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SearchResultCategory.authors => Container(),
|
SearchResultCategory.authors => Container(),
|
||||||
SearchResultCategory.series => Container(),
|
SearchResultCategory.series => Container(),
|
||||||
SearchResultCategory.tags => Container(),
|
SearchResultCategory.tags => Container(),
|
||||||
|
|
@ -71,12 +60,8 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
child: CircularProgressIndicator(),
|
error: (error, stackTrace) => Center(child: Text('Error: $error')),
|
||||||
),
|
|
||||||
error: (error, stackTrace) => Center(
|
|
||||||
child: Text('Error: $error'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,7 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
import 'package:vaani/shared/utils.dart';
|
import 'package:vaani/shared/utils.dart';
|
||||||
|
|
||||||
class LibraryItemActions extends HookConsumerWidget {
|
class LibraryItemActions extends HookConsumerWidget {
|
||||||
const LibraryItemActions({
|
const LibraryItemActions({super.key, required this.id});
|
||||||
super.key,
|
|
||||||
required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
|
|
@ -68,9 +65,7 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
// read list button
|
// read list button
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.playlist_add_rounded),
|
||||||
Icons.playlist_add_rounded,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// share button
|
// share button
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -79,8 +74,9 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
var currentServerUrl =
|
var currentServerUrl =
|
||||||
apiSettings.activeServer!.serverUrl;
|
apiSettings.activeServer!.serverUrl;
|
||||||
if (!currentServerUrl.hasScheme) {
|
if (!currentServerUrl.hasScheme) {
|
||||||
currentServerUrl =
|
currentServerUrl = Uri.https(
|
||||||
Uri.https(currentServerUrl.toString());
|
currentServerUrl.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
handleLaunchUrl(
|
handleLaunchUrl(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
|
|
@ -140,7 +136,8 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
.database
|
.database
|
||||||
.deleteRecordWithId(
|
.deleteRecordWithId(
|
||||||
record
|
record
|
||||||
.task.taskId,
|
.task
|
||||||
|
.taskId,
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
|
|
@ -161,8 +158,8 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
// open the file location
|
// open the file location
|
||||||
final didOpen =
|
final didOpen =
|
||||||
await FileDownloader().openFile(
|
await FileDownloader().openFile(
|
||||||
task: record.task,
|
task: record.task,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!didOpen) {
|
if (!didOpen) {
|
||||||
appLogger.warning(
|
appLogger.warning(
|
||||||
|
|
@ -182,16 +179,13 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => Center(
|
error: (error, stackTrace) =>
|
||||||
child: Text('Error: $error'),
|
Center(child: Text('Error: $error')),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
Icons.more_vert_rounded,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -206,10 +200,7 @@ class LibraryItemActions extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LibItemDownloadButton extends HookConsumerWidget {
|
class LibItemDownloadButton extends HookConsumerWidget {
|
||||||
const LibItemDownloadButton({
|
const LibItemDownloadButton({super.key, required this.item});
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final shelfsdk.LibraryItemExpanded item;
|
final shelfsdk.LibraryItemExpanded item;
|
||||||
|
|
||||||
|
|
@ -222,9 +213,7 @@ class LibItemDownloadButton extends HookConsumerWidget {
|
||||||
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
|
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
|
||||||
|
|
||||||
return isItemDownloading
|
return isItemDownloading
|
||||||
? ItemCurrentlyInDownloadQueue(
|
? ItemCurrentlyInDownloadQueue(item: item)
|
||||||
item: item,
|
|
||||||
)
|
|
||||||
: IconButton(
|
: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
appLogger.fine('Pressed download button');
|
appLogger.fine('Pressed download button');
|
||||||
|
|
@ -233,18 +222,13 @@ class LibItemDownloadButton extends HookConsumerWidget {
|
||||||
.read(downloadManagerProvider.notifier)
|
.read(downloadManagerProvider.notifier)
|
||||||
.queueAudioBookDownload(item);
|
.queueAudioBookDownload(item);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.download_rounded),
|
||||||
Icons.download_rounded,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
|
class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
|
||||||
const ItemCurrentlyInDownloadQueue({
|
const ItemCurrentlyInDownloadQueue({super.key, required this.item});
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final shelfsdk.LibraryItemExpanded item;
|
final shelfsdk.LibraryItemExpanded item;
|
||||||
|
|
||||||
|
|
@ -263,17 +247,12 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(value: progress, strokeWidth: 2),
|
||||||
value: progress,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.download,
|
Icons.download,
|
||||||
// color: Theme.of(context).progressIndicatorTheme.color,
|
// color: Theme.of(context).progressIndicatorTheme.color,
|
||||||
)
|
|
||||||
.animate(
|
|
||||||
onPlay: (controller) => controller.repeat(),
|
|
||||||
)
|
)
|
||||||
|
.animate(onPlay: (controller) => controller.repeat())
|
||||||
.fade(
|
.fade(
|
||||||
duration: shimmerDuration,
|
duration: shimmerDuration,
|
||||||
end: 1,
|
end: 1,
|
||||||
|
|
@ -292,10 +271,7 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlreadyItemDownloadedButton extends HookConsumerWidget {
|
class AlreadyItemDownloadedButton extends HookConsumerWidget {
|
||||||
const AlreadyItemDownloadedButton({
|
const AlreadyItemDownloadedButton({super.key, required this.item});
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final shelfsdk.LibraryItemExpanded item;
|
final shelfsdk.LibraryItemExpanded item;
|
||||||
|
|
||||||
|
|
@ -317,25 +293,18 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget {
|
||||||
top: 8.0,
|
top: 8.0,
|
||||||
bottom: (isBookPlaying ? playerMinHeight : 0) + 8,
|
bottom: (isBookPlaying ? playerMinHeight : 0) + 8,
|
||||||
),
|
),
|
||||||
child: DownloadSheet(
|
child: DownloadSheet(item: item),
|
||||||
item: item,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.download_done_rounded),
|
||||||
Icons.download_done_rounded,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadSheet extends HookConsumerWidget {
|
class DownloadSheet extends HookConsumerWidget {
|
||||||
const DownloadSheet({
|
const DownloadSheet({super.key, required this.item});
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final shelfsdk.LibraryItemExpanded item;
|
final shelfsdk.LibraryItemExpanded item;
|
||||||
|
|
||||||
|
|
@ -367,9 +336,7 @@ class DownloadSheet extends HookConsumerWidget {
|
||||||
// ),
|
// ),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Delete'),
|
title: const Text('Delete'),
|
||||||
leading: const Icon(
|
leading: const Icon(Icons.delete_rounded),
|
||||||
Icons.delete_rounded,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
// show the delete dialog
|
// show the delete dialog
|
||||||
final wasDeleted = await showDialog<bool>(
|
final wasDeleted = await showDialog<bool>(
|
||||||
|
|
@ -387,9 +354,7 @@ class DownloadSheet extends HookConsumerWidget {
|
||||||
// delete the file
|
// delete the file
|
||||||
ref
|
ref
|
||||||
.read(downloadManagerProvider.notifier)
|
.read(downloadManagerProvider.notifier)
|
||||||
.deleteDownloadedItem(
|
.deleteDownloadedItem(item);
|
||||||
item,
|
|
||||||
);
|
|
||||||
GoRouter.of(context).pop(true);
|
GoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
child: const Text('Yes'),
|
child: const Text('Yes'),
|
||||||
|
|
@ -409,11 +374,7 @@ class DownloadSheet extends HookConsumerWidget {
|
||||||
appLogger.fine('Deleted ${item.media.metadata.title}');
|
appLogger.fine('Deleted ${item.media.metadata.title}');
|
||||||
GoRouter.of(context).pop();
|
GoRouter.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text('Deleted ${item.media.metadata.title}')),
|
||||||
content: Text(
|
|
||||||
'Deleted ${item.media.metadata.title}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -424,9 +385,7 @@ class DownloadSheet extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LibraryItemPlayButton extends HookConsumerWidget {
|
class _LibraryItemPlayButton extends HookConsumerWidget {
|
||||||
const _LibraryItemPlayButton({
|
const _LibraryItemPlayButton({required this.item});
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final shelfsdk.LibraryItemExpanded item;
|
final shelfsdk.LibraryItemExpanded item;
|
||||||
|
|
||||||
|
|
@ -477,9 +436,7 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
label: Text(getPlayDisplayText()),
|
label: Text(getPlayDisplayText()),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -502,11 +459,11 @@ class DynamicItemPlayIcon extends StatelessWidget {
|
||||||
return Icon(
|
return Icon(
|
||||||
isCurrentBookSetInPlayer
|
isCurrentBookSetInPlayer
|
||||||
? isPlayingThisBook
|
? isPlayingThisBook
|
||||||
? Icons.pause_rounded
|
? Icons.pause_rounded
|
||||||
: Icons.play_arrow_rounded
|
: Icons.play_arrow_rounded
|
||||||
: isBookCompleted
|
: isBookCompleted
|
||||||
? Icons.replay_rounded
|
? Icons.replay_rounded
|
||||||
: Icons.play_arrow_rounded,
|
: Icons.play_arrow_rounded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,8 +486,9 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
appLogger.info('Setting the book ${book.libraryItemId}');
|
appLogger.info('Setting the book ${book.libraryItemId}');
|
||||||
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
|
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
|
||||||
final downloadManager = ref.watch(simpleDownloadManagerProvider);
|
final downloadManager = ref.watch(simpleDownloadManagerProvider);
|
||||||
final libItem =
|
final libItem = await ref.read(
|
||||||
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
libraryItemProvider(book.libraryItemId).future,
|
||||||
|
);
|
||||||
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
||||||
setSourceFuture = player.setSourceAudiobook(
|
setSourceFuture = player.setSourceAudiobook(
|
||||||
book,
|
book,
|
||||||
|
|
@ -546,8 +504,9 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// set the volume as this is the first time playing and dismissing causes the volume to go to 0
|
// set the volume as this is the first time playing and dismissing causes the volume to go to 0
|
||||||
var bookPlayerSettings =
|
var bookPlayerSettings = ref
|
||||||
ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings;
|
.read(bookSettingsProvider(book.libraryItemId))
|
||||||
|
.playerSettings;
|
||||||
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
|
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
|
||||||
|
|
||||||
var configurePlayerForEveryBook =
|
var configurePlayerForEveryBook =
|
||||||
|
|
@ -559,14 +518,14 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
player.setVolume(
|
player.setVolume(
|
||||||
configurePlayerForEveryBook
|
configurePlayerForEveryBook
|
||||||
? bookPlayerSettings.preferredDefaultVolume ??
|
? bookPlayerSettings.preferredDefaultVolume ??
|
||||||
appPlayerSettings.preferredDefaultVolume
|
appPlayerSettings.preferredDefaultVolume
|
||||||
: appPlayerSettings.preferredDefaultVolume,
|
: appPlayerSettings.preferredDefaultVolume,
|
||||||
),
|
),
|
||||||
// set the speed
|
// set the speed
|
||||||
player.setSpeed(
|
player.setSpeed(
|
||||||
configurePlayerForEveryBook
|
configurePlayerForEveryBook
|
||||||
? bookPlayerSettings.preferredDefaultSpeed ??
|
? bookPlayerSettings.preferredDefaultSpeed ??
|
||||||
appPlayerSettings.preferredDefaultSpeed
|
appPlayerSettings.preferredDefaultSpeed
|
||||||
: appPlayerSettings.preferredDefaultSpeed,
|
: appPlayerSettings.preferredDefaultSpeed,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,13 @@ class LibraryItemHeroSection extends HookConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: HeroTagPrefixes.bookCover +
|
tag:
|
||||||
|
HeroTagPrefixes.bookCover +
|
||||||
itemId +
|
itemId +
|
||||||
(extraMap?.heroTagSuffix ?? ''),
|
(extraMap?.heroTagSuffix ?? ''),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: _BookCover(
|
child: _BookCover(itemId: itemId),
|
||||||
itemId: itemId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// a progress bar
|
// a progress bar
|
||||||
|
|
@ -59,9 +58,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
|
||||||
right: 8.0,
|
right: 8.0,
|
||||||
left: 8.0,
|
left: 8.0,
|
||||||
),
|
),
|
||||||
child: _LibraryItemProgressIndicator(
|
child: _LibraryItemProgressIndicator(id: itemId),
|
||||||
id: itemId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -77,10 +74,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BookDetails extends HookConsumerWidget {
|
class _BookDetails extends HookConsumerWidget {
|
||||||
const _BookDetails({
|
const _BookDetails({required this.id, this.extraMap});
|
||||||
required this.id,
|
|
||||||
this.extraMap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final LibraryItemExtras? extraMap;
|
final LibraryItemExtras? extraMap;
|
||||||
|
|
@ -99,10 +93,7 @@ class _BookDetails extends HookConsumerWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_BookTitle(
|
_BookTitle(extraMap: extraMap, itemBookMetadata: itemBookMetadata),
|
||||||
extraMap: extraMap,
|
|
||||||
itemBookMetadata: itemBookMetadata,
|
|
||||||
),
|
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -134,9 +125,7 @@ class _BookDetails extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
const _LibraryItemProgressIndicator({
|
const _LibraryItemProgressIndicator({required this.id});
|
||||||
required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
|
|
@ -157,13 +146,15 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
Duration remainingTime;
|
Duration remainingTime;
|
||||||
if (player.book?.libraryItemId == libraryItem.id) {
|
if (player.book?.libraryItemId == libraryItem.id) {
|
||||||
// final positionStream = useStream(player.slowPositionStream);
|
// final positionStream = useStream(player.slowPositionStream);
|
||||||
progress = (player.positionInBook).inSeconds /
|
progress =
|
||||||
|
(player.positionInBook).inSeconds /
|
||||||
libraryItem.media.asBookExpanded.duration.inSeconds;
|
libraryItem.media.asBookExpanded.duration.inSeconds;
|
||||||
remainingTime =
|
remainingTime =
|
||||||
libraryItem.media.asBookExpanded.duration - player.positionInBook;
|
libraryItem.media.asBookExpanded.duration - player.positionInBook;
|
||||||
} else {
|
} else {
|
||||||
progress = mediaProgress?.progress ?? 0;
|
progress = mediaProgress?.progress ?? 0;
|
||||||
remainingTime = (libraryItem.media.asBookExpanded.duration -
|
remainingTime =
|
||||||
|
(libraryItem.media.asBookExpanded.duration -
|
||||||
mediaProgress!.currentTime);
|
mediaProgress!.currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,20 +181,17 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
semanticsLabel: 'Book progress',
|
semanticsLabel: 'Book progress',
|
||||||
semanticsValue: '${progressInPercent.toStringAsFixed(2)}%',
|
semanticsValue: '${progressInPercent.toStringAsFixed(2)}%',
|
||||||
),
|
),
|
||||||
const SizedBox.square(
|
const SizedBox.square(dimension: 4.0),
|
||||||
dimension: 4.0,
|
|
||||||
),
|
|
||||||
// time remaining
|
// time remaining
|
||||||
Text(
|
Text(
|
||||||
// only show 2 decimal places
|
// only show 2 decimal places
|
||||||
'${remainingTime.smartBinaryFormat} left',
|
'${remainingTime.smartBinaryFormat} left',
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.75),
|
||||||
.withValues(alpha: 0.75),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -212,10 +200,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
const _HeroSectionSubLabelWithIcon({
|
const _HeroSectionSubLabelWithIcon({required this.icon, required this.text});
|
||||||
required this.icon,
|
|
||||||
required this.text,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Widget text;
|
final Widget text;
|
||||||
|
|
@ -225,8 +210,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
final themeData = Theme.of(context);
|
final themeData = Theme.of(context);
|
||||||
final useFontAwesome =
|
final useFontAwesome =
|
||||||
icon.runtimeType == FontAwesomeIcons.book.runtimeType;
|
icon.runtimeType == FontAwesomeIcons.book.runtimeType;
|
||||||
final useMaterialThemeOnItemPage =
|
final useMaterialThemeOnItemPage = ref
|
||||||
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
|
.watch(appSettingsProvider)
|
||||||
|
.themeSettings
|
||||||
|
.useMaterialThemeOnItemPage;
|
||||||
final color = useMaterialThemeOnItemPage
|
final color = useMaterialThemeOnItemPage
|
||||||
? themeData.colorScheme.primary
|
? themeData.colorScheme.primary
|
||||||
: themeData.colorScheme.onSurface.withValues(alpha: 0.75);
|
: themeData.colorScheme.onSurface.withValues(alpha: 0.75);
|
||||||
|
|
@ -237,20 +224,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(right: 8, top: 2),
|
margin: const EdgeInsets.only(right: 8, top: 2),
|
||||||
child: useFontAwesome
|
child: useFontAwesome
|
||||||
? FaIcon(
|
? FaIcon(icon, size: 16, color: color)
|
||||||
icon,
|
: Icon(icon, size: 16, color: color),
|
||||||
size: 16,
|
|
||||||
color: color,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
icon,
|
|
||||||
size: 16,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: text,
|
|
||||||
),
|
),
|
||||||
|
Expanded(child: text),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -338,9 +315,7 @@ class _BookNarrators extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BookCover extends HookConsumerWidget {
|
class _BookCover extends HookConsumerWidget {
|
||||||
const _BookCover({
|
const _BookCover({required this.itemId});
|
||||||
required this.itemId,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String itemId;
|
final String itemId;
|
||||||
|
|
||||||
|
|
@ -358,7 +333,8 @@ class _BookCover extends HookConsumerWidget {
|
||||||
themeOfLibraryItemProvider(
|
themeOfLibraryItemProvider(
|
||||||
itemId,
|
itemId,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
highContrast: themeSettings.highContrast ||
|
highContrast:
|
||||||
|
themeSettings.highContrast ||
|
||||||
MediaQuery.of(context).highContrast,
|
MediaQuery.of(context).highContrast,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -391,15 +367,10 @@ class _BookCover extends HookConsumerWidget {
|
||||||
return const Icon(Icons.error);
|
return const Icon(Icons.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Image.memory(
|
return Image.memory(image, fit: BoxFit.cover);
|
||||||
image,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
loading: () {
|
loading: () {
|
||||||
return const Center(
|
return const Center(child: BookCoverSkeleton());
|
||||||
child: BookCoverSkeleton(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
error: (error, stack) {
|
error: (error, stack) {
|
||||||
return const Center(child: Icon(Icons.error));
|
return const Center(child: Icon(Icons.error));
|
||||||
|
|
@ -411,10 +382,7 @@ class _BookCover extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BookTitle extends StatelessWidget {
|
class _BookTitle extends StatelessWidget {
|
||||||
const _BookTitle({
|
const _BookTitle({required this.extraMap, required this.itemBookMetadata});
|
||||||
required this.extraMap,
|
|
||||||
required this.itemBookMetadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
final LibraryItemExtras? extraMap;
|
final LibraryItemExtras? extraMap;
|
||||||
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
|
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
|
||||||
|
|
@ -426,7 +394,8 @@ class _BookTitle extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: HeroTagPrefixes.bookTitle +
|
tag:
|
||||||
|
HeroTagPrefixes.bookTitle +
|
||||||
// itemId +
|
// itemId +
|
||||||
(extraMap?.heroTagSuffix ?? ''),
|
(extraMap?.heroTagSuffix ?? ''),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@ import 'package:vaani/api/library_item_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
|
||||||
class LibraryItemMetadata extends HookConsumerWidget {
|
class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
const LibraryItemMetadata({
|
const LibraryItemMetadata({super.key, required this.id});
|
||||||
super.key,
|
|
||||||
required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
|
|
@ -72,7 +69,8 @@ class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
_MetadataItem(
|
_MetadataItem(
|
||||||
title: 'Published',
|
title: 'Published',
|
||||||
value: itemBookMetadata?.publishedDate ??
|
value:
|
||||||
|
itemBookMetadata?.publishedDate ??
|
||||||
itemBookMetadata?.publishedYear ??
|
itemBookMetadata?.publishedYear ??
|
||||||
'Unknown',
|
'Unknown',
|
||||||
),
|
),
|
||||||
|
|
@ -87,22 +85,18 @@ class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
// alternate between metadata and vertical divider
|
// alternate between metadata and vertical divider
|
||||||
children: List.generate(
|
children: List.generate(children.length * 2 - 1, (index) {
|
||||||
children.length * 2 - 1,
|
if (index.isEven) {
|
||||||
(index) {
|
return children[index ~/ 2];
|
||||||
if (index.isEven) {
|
}
|
||||||
return children[index ~/ 2];
|
return VerticalDivider(
|
||||||
}
|
indent: 6,
|
||||||
return VerticalDivider(
|
endIndent: 6,
|
||||||
indent: 6,
|
color: Theme.of(
|
||||||
endIndent: 6,
|
context,
|
||||||
color: Theme.of(context)
|
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
.colorScheme
|
);
|
||||||
.onSurface
|
}),
|
||||||
.withValues(alpha: 0.6),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -111,10 +105,7 @@ class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
|
|
||||||
/// key-value pair to display as column
|
/// key-value pair to display as column
|
||||||
class _MetadataItem extends StatelessWidget {
|
class _MetadataItem extends StatelessWidget {
|
||||||
const _MetadataItem({
|
const _MetadataItem({required this.title, required this.value});
|
||||||
required this.title,
|
|
||||||
required this.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String value;
|
final String value;
|
||||||
|
|
|
||||||
|
|
@ -16,49 +16,43 @@ import 'library_item_hero_section.dart';
|
||||||
import 'library_item_metadata.dart';
|
import 'library_item_metadata.dart';
|
||||||
|
|
||||||
class LibraryItemPage extends HookConsumerWidget {
|
class LibraryItemPage extends HookConsumerWidget {
|
||||||
const LibraryItemPage({
|
const LibraryItemPage({super.key, required this.itemId, this.extra});
|
||||||
super.key,
|
|
||||||
required this.itemId,
|
|
||||||
this.extra,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String itemId;
|
final String itemId;
|
||||||
final Object? extra;
|
final Object? extra;
|
||||||
static const double _showFabThreshold = 300.0;
|
static const double _showFabThreshold = 300.0;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final additionalItemData =
|
final additionalItemData = extra is LibraryItemExtras
|
||||||
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
|
? extra as LibraryItemExtras
|
||||||
|
: null;
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final showFab = useState(false);
|
final showFab = useState(false);
|
||||||
|
|
||||||
// Effect to listen to scroll changes and update FAB visibility
|
// Effect to listen to scroll changes and update FAB visibility
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
void listener() {
|
||||||
void listener() {
|
if (!scrollController.hasClients) {
|
||||||
if (!scrollController.hasClients) {
|
return; // Ensure controller is attached
|
||||||
return; // Ensure controller is attached
|
|
||||||
}
|
|
||||||
final shouldShow = scrollController.offset > _showFabThreshold;
|
|
||||||
// Update state only if it changes and widget is still mounted
|
|
||||||
if (showFab.value != shouldShow && context.mounted) {
|
|
||||||
showFab.value = shouldShow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
final shouldShow = scrollController.offset > _showFabThreshold;
|
||||||
|
// Update state only if it changes and widget is still mounted
|
||||||
|
if (showFab.value != shouldShow && context.mounted) {
|
||||||
|
showFab.value = shouldShow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scrollController.addListener(listener);
|
scrollController.addListener(listener);
|
||||||
// Initial check in case the view starts scrolled (less likely but safe)
|
// Initial check in case the view starts scrolled (less likely but safe)
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (scrollController.hasClients && context.mounted) {
|
if (scrollController.hasClients && context.mounted) {
|
||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup: remove the listener when the widget is disposed
|
// Cleanup: remove the listener when the widget is disposed
|
||||||
return () => scrollController.removeListener(listener);
|
return () => scrollController.removeListener(listener);
|
||||||
},
|
}, [scrollController]); // Re-run effect if scrollController changes
|
||||||
[scrollController],
|
|
||||||
); // Re-run effect if scrollController changes
|
|
||||||
|
|
||||||
// --- FAB Scroll-to-Top Logic ---
|
// --- FAB Scroll-to-Top Logic ---
|
||||||
void scrollToTop() {
|
void scrollToTop() {
|
||||||
|
|
@ -82,10 +76,7 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||||
return ScaleTransition(
|
return ScaleTransition(
|
||||||
scale: animation,
|
scale: animation,
|
||||||
child: FadeTransition(
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: showFab.value
|
child: showFab.value
|
||||||
|
|
@ -96,9 +87,7 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
tooltip: 'Scroll to top',
|
tooltip: 'Scroll to top',
|
||||||
child: const Icon(Icons.arrow_upward),
|
child: const Icon(Icons.arrow_upward),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(
|
: const SizedBox.shrink(key: ValueKey('fab-empty')),
|
||||||
key: ValueKey('fab-empty'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
|
@ -115,17 +104,11 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// a horizontal display with dividers of metadata
|
// a horizontal display with dividers of metadata
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(child: LibraryItemMetadata(id: itemId)),
|
||||||
child: LibraryItemMetadata(id: itemId),
|
|
||||||
),
|
|
||||||
// a row of actions like play, download, share, etc
|
// a row of actions like play, download, share, etc
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(child: LibraryItemActions(id: itemId)),
|
||||||
child: LibraryItemActions(id: itemId),
|
|
||||||
),
|
|
||||||
// a expandable section for book description
|
// a expandable section for book description
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(child: LibraryItemDescription(id: itemId)),
|
||||||
child: LibraryItemDescription(id: itemId),
|
|
||||||
),
|
|
||||||
// a padding at the bottom to make sure the last item is not hidden by mini player
|
// a padding at the bottom to make sure the last item is not hidden by mini player
|
||||||
const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
|
const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
|
||||||
],
|
],
|
||||||
|
|
@ -137,10 +120,7 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LibraryItemDescription extends HookConsumerWidget {
|
class LibraryItemDescription extends HookConsumerWidget {
|
||||||
const LibraryItemDescription({
|
const LibraryItemDescription({super.key, required this.id});
|
||||||
super.key,
|
|
||||||
required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@override
|
@override
|
||||||
|
|
@ -160,16 +140,21 @@ class LibraryItemDescription extends HookConsumerWidget {
|
||||||
double calculateWidth(
|
double calculateWidth(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
BoxConstraints constraints, {
|
BoxConstraints constraints, {
|
||||||
|
|
||||||
/// width ratio of the cover image to the available width
|
/// width ratio of the cover image to the available width
|
||||||
double widthRatio = 0.4,
|
double widthRatio = 0.4,
|
||||||
|
|
||||||
/// height ratio of the cover image to the available height
|
/// height ratio of the cover image to the available height
|
||||||
double maxHeightToUse = 0.25,
|
double maxHeightToUse = 0.25,
|
||||||
}) {
|
}) {
|
||||||
final availHeight =
|
final availHeight = min(
|
||||||
min(constraints.maxHeight, MediaQuery.of(context).size.height);
|
constraints.maxHeight,
|
||||||
final availWidth =
|
MediaQuery.of(context).size.height,
|
||||||
min(constraints.maxWidth, MediaQuery.of(context).size.width);
|
);
|
||||||
|
final availWidth = min(
|
||||||
|
constraints.maxWidth,
|
||||||
|
MediaQuery.of(context).size.width,
|
||||||
|
);
|
||||||
|
|
||||||
// make the width widthRatio of the available width
|
// make the width widthRatio of the available width
|
||||||
var width = availWidth * widthRatio;
|
var width = availWidth * widthRatio;
|
||||||
|
|
|
||||||
|
|
@ -21,28 +21,26 @@ class LibraryItemSliverAppBar extends HookConsumerWidget {
|
||||||
|
|
||||||
final showTitle = useState(false);
|
final showTitle = useState(false);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
void listener() {
|
||||||
void listener() {
|
final shouldShow =
|
||||||
final shouldShow = scrollController.hasClients &&
|
scrollController.hasClients &&
|
||||||
scrollController.offset > _showTitleThreshold;
|
scrollController.offset > _showTitleThreshold;
|
||||||
if (showTitle.value != shouldShow) {
|
if (showTitle.value != shouldShow) {
|
||||||
showTitle.value = shouldShow;
|
showTitle.value = shouldShow;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scrollController.addListener(listener);
|
scrollController.addListener(listener);
|
||||||
// Trigger listener once initially in case the view starts scrolled
|
// Trigger listener once initially in case the view starts scrolled
|
||||||
// (though unlikely for this specific use case, it's good practice)
|
// (though unlikely for this specific use case, it's good practice)
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (scrollController.hasClients) {
|
if (scrollController.hasClients) {
|
||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => scrollController.removeListener(listener);
|
return () => scrollController.removeListener(listener);
|
||||||
},
|
}, [scrollController]);
|
||||||
[scrollController],
|
|
||||||
);
|
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|
|
||||||
|
|
@ -41,43 +41,41 @@ class LibraryBrowserPage extends HookConsumerWidget {
|
||||||
title: Text(appBarTitle),
|
title: Text(appBarTitle),
|
||||||
),
|
),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate([
|
||||||
[
|
ListTile(
|
||||||
ListTile(
|
title: const Text('Authors'),
|
||||||
title: const Text('Authors'),
|
leading: const Icon(Icons.person),
|
||||||
leading: const Icon(Icons.person),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
onTap: () {
|
||||||
onTap: () {
|
showNotImplementedToast(context);
|
||||||
showNotImplementedToast(context);
|
},
|
||||||
},
|
),
|
||||||
),
|
ListTile(
|
||||||
ListTile(
|
title: const Text('Genres'),
|
||||||
title: const Text('Genres'),
|
leading: const Icon(Icons.category),
|
||||||
leading: const Icon(Icons.category),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
onTap: () {
|
||||||
onTap: () {
|
showNotImplementedToast(context);
|
||||||
showNotImplementedToast(context);
|
},
|
||||||
},
|
),
|
||||||
),
|
ListTile(
|
||||||
ListTile(
|
title: const Text('Series'),
|
||||||
title: const Text('Series'),
|
leading: const Icon(Icons.list),
|
||||||
leading: const Icon(Icons.list),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
onTap: () {
|
||||||
onTap: () {
|
showNotImplementedToast(context);
|
||||||
showNotImplementedToast(context);
|
},
|
||||||
},
|
),
|
||||||
),
|
// Downloads
|
||||||
// Downloads
|
ListTile(
|
||||||
ListTile(
|
title: const Text('Downloads'),
|
||||||
title: const Text('Downloads'),
|
leading: const Icon(Icons.download),
|
||||||
leading: const Icon(Icons.download),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
onTap: () {
|
||||||
onTap: () {
|
GoRouter.of(context).pushNamed(Routes.downloads.name);
|
||||||
GoRouter.of(context).pushNamed(Routes.downloads.name);
|
},
|
||||||
},
|
),
|
||||||
),
|
]),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,10 @@ String generateZipFileName() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Level parseLevel(String level) {
|
Level parseLevel(String level) {
|
||||||
return Level.LEVELS
|
return Level.LEVELS.firstWhere(
|
||||||
.firstWhere((l) => l.name == level, orElse: () => Level.ALL);
|
(l) => l.name == level,
|
||||||
|
orElse: () => Level.ALL,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogRecord parseLogLine(String line) {
|
LogRecord parseLogLine(String line) {
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,9 @@ class LogsPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
appLogger.info('Preparing logs for sharing');
|
appLogger.info('Preparing logs for sharing');
|
||||||
final zipLogFilePath =
|
final zipLogFilePath = await ref
|
||||||
await ref.read(logsProvider.notifier).getZipFilePath();
|
.read(logsProvider.notifier)
|
||||||
|
.getZipFilePath();
|
||||||
|
|
||||||
// submit logs
|
// submit logs
|
||||||
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
|
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
|
||||||
|
|
@ -169,7 +170,6 @@ class LogsPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
// a filter for log levels, loggers, and search
|
// a filter for log levels, loggers, and search
|
||||||
// TODO: implement filters and search
|
// TODO: implement filters and search
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: logs.when(
|
child: logs.when(
|
||||||
data: (logRecords) {
|
data: (logRecords) {
|
||||||
|
|
@ -243,9 +243,7 @@ class LogRecordTile extends StatelessWidget {
|
||||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n\n'),
|
const TextSpan(text: '\n\n'),
|
||||||
TextSpan(
|
TextSpan(text: logRecord.message),
|
||||||
text: logRecord.message,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,10 @@ class OauthFlows extends _$OauthFlows {
|
||||||
}
|
}
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
oauthState: state[oauthState]!
|
oauthState: state[oauthState]!.copyWith(
|
||||||
.copyWith(isFlowComplete: true, authToken: authToken),
|
isFlowComplete: true,
|
||||||
|
authToken: authToken,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ class CallbackPage extends HookConsumerWidget {
|
||||||
|
|
||||||
// check if the state is in the flows
|
// check if the state is in the flows
|
||||||
if (!flows.containsKey(state)) {
|
if (!flows.containsKey(state)) {
|
||||||
return const _SomethingWentWrong(
|
return const _SomethingWentWrong(message: 'State not found');
|
||||||
message: 'State not found',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the token
|
// get the token
|
||||||
|
|
@ -45,26 +43,21 @@ class CallbackPage extends HookConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('Contacting server...\nPlease wait\n\nGot:'
|
Text(
|
||||||
'\nState: $state\nCode: $code'),
|
'Contacting server...\nPlease wait\n\nGot:'
|
||||||
|
'\nState: $state\nCode: $code',
|
||||||
|
),
|
||||||
loginAuthToken.when(
|
loginAuthToken.when(
|
||||||
data: (authenticationToken) {
|
data: (authenticationToken) {
|
||||||
if (authenticationToken == null) {
|
if (authenticationToken == null) {
|
||||||
handleServerError(
|
handleServerError(context, serverErrorResponse);
|
||||||
context,
|
|
||||||
serverErrorResponse,
|
|
||||||
);
|
|
||||||
return const BackToLoginButton();
|
return const BackToLoginButton();
|
||||||
}
|
}
|
||||||
return Text('Token: $authenticationToken');
|
return Text('Token: $authenticationToken');
|
||||||
},
|
},
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const CircularProgressIndicator(),
|
||||||
error: (error, _) {
|
error: (error, _) {
|
||||||
handleServerError(
|
handleServerError(context, serverErrorResponse, e: error);
|
||||||
context,
|
|
||||||
serverErrorResponse,
|
|
||||||
e: error,
|
|
||||||
);
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text('Error with OAuth flow: $error'),
|
Text('Error with OAuth flow: $error'),
|
||||||
|
|
@ -81,9 +74,7 @@ class CallbackPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackToLoginButton extends StatelessWidget {
|
class BackToLoginButton extends StatelessWidget {
|
||||||
const BackToLoginButton({
|
const BackToLoginButton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -97,9 +88,7 @@ class BackToLoginButton extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SomethingWentWrong extends StatelessWidget {
|
class _SomethingWentWrong extends StatelessWidget {
|
||||||
const _SomethingWentWrong({
|
const _SomethingWentWrong({this.message = 'Error with OAuth flow'});
|
||||||
this.message = 'Error with OAuth flow',
|
|
||||||
});
|
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
|
@ -109,10 +98,7 @@ class _SomethingWentWrong extends StatelessWidget {
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [Text(message), const BackToLoginButton()],
|
||||||
Text(message),
|
|
||||||
const BackToLoginButton(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import 'package:vaani/shared/utils.dart';
|
||||||
import 'package:vaani/shared/widgets/add_new_server.dart';
|
import 'package:vaani/shared/widgets/add_new_server.dart';
|
||||||
|
|
||||||
class OnboardingSinglePage extends HookConsumerWidget {
|
class OnboardingSinglePage extends HookConsumerWidget {
|
||||||
const OnboardingSinglePage({
|
const OnboardingSinglePage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -23,8 +21,9 @@ class OnboardingSinglePage extends HookConsumerWidget {
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
minWidth:
|
minWidth: constraints.maxWidth < 600
|
||||||
constraints.maxWidth < 600 ? constraints.maxWidth : 0,
|
? constraints.maxWidth
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
child: const Padding(
|
child: const Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 20.0),
|
padding: EdgeInsets.symmetric(vertical: 20.0),
|
||||||
|
|
@ -39,10 +38,7 @@ class OnboardingSinglePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget fadeSlideTransitionBuilder(
|
Widget fadeSlideTransitionBuilder(Widget child, Animation<double> animation) {
|
||||||
Widget child,
|
|
||||||
Animation<double> animation,
|
|
||||||
) {
|
|
||||||
return FadeTransition(
|
return FadeTransition(
|
||||||
opacity: animation,
|
opacity: animation,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
|
|
@ -56,9 +52,7 @@ Widget fadeSlideTransitionBuilder(
|
||||||
}
|
}
|
||||||
|
|
||||||
class OnboardingBody extends HookConsumerWidget {
|
class OnboardingBody extends HookConsumerWidget {
|
||||||
const OnboardingBody({
|
const OnboardingBody({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -81,9 +75,7 @@ class OnboardingBody extends HookConsumerWidget {
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox.square(
|
const SizedBox.square(dimension: 16.0),
|
||||||
dimension: 16.0,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
|
@ -112,21 +104,17 @@ class OnboardingBody extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox.square(
|
const SizedBox.square(dimension: 16.0),
|
||||||
dimension: 16.0,
|
|
||||||
),
|
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: 500.ms,
|
duration: 500.ms,
|
||||||
transitionBuilder: fadeSlideTransitionBuilder,
|
transitionBuilder: fadeSlideTransitionBuilder,
|
||||||
child: canUserLogin.value
|
child: canUserLogin.value
|
||||||
? UserLoginWidget(
|
? UserLoginWidget(server: audiobookshelfUri)
|
||||||
server: audiobookshelfUri,
|
|
||||||
)
|
|
||||||
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
|
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
|
||||||
: const RedirectToABS().animate().fadeIn().slideY(
|
: const RedirectToABS().animate().fadeIn().slideY(
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
duration: 500.ms,
|
duration: 500.ms,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -134,9 +122,7 @@ class OnboardingBody extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RedirectToABS extends StatelessWidget {
|
class RedirectToABS extends StatelessWidget {
|
||||||
const RedirectToABS({
|
const RedirectToABS({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -152,18 +138,14 @@ class RedirectToABS extends StatelessWidget {
|
||||||
isSemanticButton: false,
|
isSemanticButton: false,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
elevation: WidgetStateProperty.all(0),
|
elevation: WidgetStateProperty.all(0),
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStateProperty.all(const EdgeInsets.all(0)),
|
||||||
const EdgeInsets.all(0),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// open the github page
|
// open the github page
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('Opening the github page');
|
print('Opening the github page');
|
||||||
await handleLaunchUrl(
|
await handleLaunchUrl(
|
||||||
Uri.parse(
|
Uri.parse('https://www.audiobookshelf.org'),
|
||||||
'https://www.audiobookshelf.org',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Click here'),
|
child: const Text('Click here'),
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,7 @@ import 'package:vaani/settings/api_settings_provider.dart'
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
|
||||||
class UserLoginWidget extends HookConsumerWidget {
|
class UserLoginWidget extends HookConsumerWidget {
|
||||||
const UserLoginWidget({
|
const UserLoginWidget({super.key, required this.server, this.onSuccess});
|
||||||
super.key,
|
|
||||||
required this.server,
|
|
||||||
this.onSuccess,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final Function(model.AuthenticatedUser)? onSuccess;
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
@ -34,8 +30,9 @@ class UserLoginWidget extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final serverStatusError = useMemoized(() => ErrorResponseHandler(), []);
|
final serverStatusError = useMemoized(() => ErrorResponseHandler(), []);
|
||||||
final serverStatus =
|
final serverStatus = ref.watch(
|
||||||
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
|
serverStatusProvider(server, serverStatusError.storeError),
|
||||||
|
);
|
||||||
|
|
||||||
return serverStatus.when(
|
return serverStatus.when(
|
||||||
data: (value) {
|
data: (value) {
|
||||||
|
|
@ -55,9 +52,7 @@ class UserLoginWidget extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () {
|
loading: () {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
error: (error, _) {
|
error: (error, _) {
|
||||||
return Center(
|
return Center(
|
||||||
|
|
@ -68,10 +63,7 @@ class UserLoginWidget extends HookConsumerWidget {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
serverStatusProvider(
|
serverStatusProvider(server, serverStatusError.storeError),
|
||||||
server,
|
|
||||||
serverStatusError.storeError,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Try again'),
|
child: const Text('Try again'),
|
||||||
|
|
@ -84,11 +76,7 @@ class UserLoginWidget extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthMethodChoice {
|
enum AuthMethodChoice { local, openid, authToken }
|
||||||
local,
|
|
||||||
openid,
|
|
||||||
authToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserLoginMultipleAuth extends HookConsumerWidget {
|
class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
const UserLoginMultipleAuth({
|
const UserLoginMultipleAuth({
|
||||||
|
|
@ -117,21 +105,17 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
model.AudiobookShelfServer addServer() {
|
model.AudiobookShelfServer addServer() {
|
||||||
var newServer = model.AudiobookShelfServer(
|
var newServer = model.AudiobookShelfServer(serverUrl: server);
|
||||||
serverUrl: server,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
// add the server to the list of servers
|
// add the server to the list of servers
|
||||||
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
ref.read(audiobookShelfServerProvider.notifier).addServer(newServer);
|
||||||
newServer,
|
|
||||||
);
|
|
||||||
} on ServerAlreadyExistsException catch (e) {
|
} on ServerAlreadyExistsException catch (e) {
|
||||||
newServer = e.server;
|
newServer = e.server;
|
||||||
} finally {
|
} finally {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
ref.read(apiSettingsProvider).copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeServer: newServer,
|
.updateState(
|
||||||
),
|
ref.read(apiSettingsProvider).copyWith(activeServer: newServer),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return newServer;
|
return newServer;
|
||||||
|
|
@ -150,42 +134,49 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
runAlignment: WrapAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children:
|
||||||
// a small label to show the user what to do
|
[
|
||||||
if (localAvailable)
|
// a small label to show the user what to do
|
||||||
ChoiceChip(
|
if (localAvailable)
|
||||||
label: const Text('Local'),
|
ChoiceChip(
|
||||||
selected: methodChoice.value == AuthMethodChoice.local,
|
label: const Text('Local'),
|
||||||
onSelected: (selected) {
|
selected:
|
||||||
if (selected) {
|
methodChoice.value ==
|
||||||
methodChoice.value = AuthMethodChoice.local;
|
AuthMethodChoice.local,
|
||||||
}
|
onSelected: (selected) {
|
||||||
},
|
if (selected) {
|
||||||
),
|
methodChoice.value = AuthMethodChoice.local;
|
||||||
if (openIDAvailable)
|
}
|
||||||
ChoiceChip(
|
},
|
||||||
label: const Text('OpenID'),
|
),
|
||||||
selected: methodChoice.value == AuthMethodChoice.openid,
|
if (openIDAvailable)
|
||||||
onSelected: (selected) {
|
ChoiceChip(
|
||||||
if (selected) {
|
label: const Text('OpenID'),
|
||||||
methodChoice.value = AuthMethodChoice.openid;
|
selected:
|
||||||
}
|
methodChoice.value ==
|
||||||
},
|
AuthMethodChoice.openid,
|
||||||
),
|
onSelected: (selected) {
|
||||||
ChoiceChip(
|
if (selected) {
|
||||||
label: const Text('Token'),
|
methodChoice.value =
|
||||||
selected:
|
AuthMethodChoice.openid;
|
||||||
methodChoice.value == AuthMethodChoice.authToken,
|
}
|
||||||
onSelected: (selected) {
|
},
|
||||||
if (selected) {
|
),
|
||||||
methodChoice.value = AuthMethodChoice.authToken;
|
ChoiceChip(
|
||||||
}
|
label: const Text('Token'),
|
||||||
},
|
selected:
|
||||||
),
|
methodChoice.value ==
|
||||||
].animate(interval: 100.ms).fadeIn(
|
AuthMethodChoice.authToken,
|
||||||
duration: 150.ms,
|
onSelected: (selected) {
|
||||||
curve: Curves.easeIn,
|
if (selected) {
|
||||||
),
|
methodChoice.value =
|
||||||
|
AuthMethodChoice.authToken;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.animate(interval: 100.ms)
|
||||||
|
.fadeIn(duration: 150.ms, curve: Curves.easeIn),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -195,21 +186,21 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
transitionBuilder: fadeSlideTransitionBuilder,
|
transitionBuilder: fadeSlideTransitionBuilder,
|
||||||
child: switch (methodChoice.value) {
|
child: switch (methodChoice.value) {
|
||||||
AuthMethodChoice.authToken => UserLoginWithToken(
|
AuthMethodChoice.authToken => UserLoginWithToken(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
AuthMethodChoice.local => UserLoginWithPassword(
|
AuthMethodChoice.local => UserLoginWithPassword(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
AuthMethodChoice.openid => UserLoginWithOpenID(
|
AuthMethodChoice.openid => UserLoginWithOpenID(
|
||||||
server: server,
|
server: server,
|
||||||
addServer: addServer,
|
addServer: addServer,
|
||||||
openIDButtonText: openIDButtonText,
|
openIDButtonText: openIDButtonText,
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
|
|
||||||
if (openIDLoginEndpoint == null) {
|
if (openIDLoginEndpoint == null) {
|
||||||
if (responseErrorHandler.response.statusCode == 400 &&
|
if (responseErrorHandler.response.statusCode == 400 &&
|
||||||
responseErrorHandler.response.body
|
responseErrorHandler.response.body.toLowerCase().contains(
|
||||||
.toLowerCase()
|
RegExp(r'invalid.*redirect.*uri'),
|
||||||
.contains(RegExp(r'invalid.*redirect.*uri'))) {
|
)) {
|
||||||
// show error
|
// show error
|
||||||
handleServerError(
|
handleServerError(
|
||||||
context,
|
context,
|
||||||
|
|
@ -97,16 +97,16 @@ class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
// add the flow to the provider
|
// add the flow to the provider
|
||||||
ref.read(oauthFlowsProvider.notifier).addFlow(
|
ref
|
||||||
|
.read(oauthFlowsProvider.notifier)
|
||||||
|
.addFlow(
|
||||||
oauthState,
|
oauthState,
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
serverUri: server,
|
serverUri: server,
|
||||||
cookie: Cookie.fromSetCookieValue(authCookie!),
|
cookie: Cookie.fromSetCookieValue(authCookie!),
|
||||||
);
|
);
|
||||||
|
|
||||||
await handleLaunchUrl(
|
await handleLaunchUrl(openIDLoginEndpoint);
|
||||||
openIDLoginEndpoint,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,14 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
final api = ref.watch(audiobookshelfApiProvider(server));
|
final api = ref.watch(audiobookshelfApiProvider(server));
|
||||||
|
|
||||||
// forward animation when the password visibility changes
|
// forward animation when the password visibility changes
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
if (isPasswordVisible.value) {
|
||||||
if (isPasswordVisible.value) {
|
isPasswordVisibleAnimationController.forward();
|
||||||
isPasswordVisibleAnimationController.forward();
|
} else {
|
||||||
} else {
|
isPasswordVisibleAnimationController.reverse();
|
||||||
isPasswordVisibleAnimationController.reverse();
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
}, [isPasswordVisible.value]);
|
||||||
},
|
|
||||||
[isPasswordVisible.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Login to the server and save the user
|
/// Login to the server and save the user
|
||||||
Future<void> loginAndSave() async {
|
Future<void> loginAndSave() async {
|
||||||
|
|
@ -109,10 +106,9 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Username',
|
labelText: 'Username',
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
.withValues(alpha: 0.8),
|
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
|
@ -129,18 +125,16 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
.withValues(alpha: 0.8),
|
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: ColorFiltered(
|
suffixIcon: ColorFiltered(
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(
|
||||||
Theme.of(context)
|
Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.primary
|
).colorScheme.primary.withValues(alpha: 0.8),
|
||||||
.withValues(alpha: 0.8),
|
|
||||||
BlendMode.srcIn,
|
BlendMode.srcIn,
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
|
@ -157,9 +151,7 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIconConstraints: const BoxConstraints(
|
suffixIconConstraints: const BoxConstraints(maxHeight: 45),
|
||||||
maxHeight: 45,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
@ -197,10 +189,12 @@ Future<void> handleServerError(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Error'),
|
title: const Text('Error'),
|
||||||
content: SelectableText('$title\n'
|
content: SelectableText(
|
||||||
'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n'
|
'$title\n'
|
||||||
'Stacktrace: $e\n\n'
|
'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n'
|
||||||
'$body\n\n'),
|
'Stacktrace: $e\n\n'
|
||||||
|
'$body\n\n',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (outLink != null)
|
if (outLink != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -214,8 +208,8 @@ Future<void> handleServerError(
|
||||||
// open an issue on the github page
|
// open an issue on the github page
|
||||||
handleLaunchUrl(
|
handleLaunchUrl(
|
||||||
AppMetadata.githubRepo
|
AppMetadata.githubRepo
|
||||||
// append the issue url
|
// append the issue url
|
||||||
.replace(
|
.replace(
|
||||||
path: '${AppMetadata.githubRepo.path}/issues/new',
|
path: '${AppMetadata.githubRepo.path}/issues/new',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,9 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'API Token',
|
labelText: 'API Token',
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
.withValues(alpha: 0.8),
|
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
|
@ -107,10 +106,7 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ElevatedButton(
|
ElevatedButton(onPressed: loginAndSave, child: const Text('Login')),
|
||||||
onPressed: loginAndSave,
|
|
||||||
child: const Text('Login'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,7 @@ class PlaybackReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tryReportPlayback(_) async {
|
Future<void> tryReportPlayback(_) async {
|
||||||
_logger.fine(
|
_logger.fine('callback called when elapsed ${_stopwatch.elapsed}');
|
||||||
'callback called when elapsed ${_stopwatch.elapsed}',
|
|
||||||
);
|
|
||||||
if (player.book != null &&
|
if (player.book != null &&
|
||||||
player.positionInBook >=
|
player.positionInBook >=
|
||||||
player.book!.duration - markCompleteWhenTimeLeft) {
|
player.book!.duration - markCompleteWhenTimeLeft) {
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ class PlaybackReporter extends _$PlaybackReporter {
|
||||||
final deviceName = await ref.watch(deviceNameProvider.future);
|
final deviceName = await ref.watch(deviceNameProvider.future);
|
||||||
final deviceModel = await ref.watch(deviceModelProvider.future);
|
final deviceModel = await ref.watch(deviceModelProvider.future);
|
||||||
final deviceSdkVersion = await ref.watch(deviceSdkVersionProvider.future);
|
final deviceSdkVersion = await ref.watch(deviceSdkVersionProvider.future);
|
||||||
final deviceManufacturer =
|
final deviceManufacturer = await ref.watch(
|
||||||
await ref.watch(deviceManufacturerProvider.future);
|
deviceManufacturerProvider.future,
|
||||||
|
);
|
||||||
|
|
||||||
final reporter = core.PlaybackReporter(
|
final reporter = core.PlaybackReporter(
|
||||||
player,
|
player,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ Duration sumOfTracks(BookExpanded book, int? index) {
|
||||||
_logger.warning('Index is null or less than 0, returning 0');
|
_logger.warning('Index is null or less than 0, returning 0');
|
||||||
return Duration.zero;
|
return Duration.zero;
|
||||||
}
|
}
|
||||||
final total = book.tracks.sublist(0, index).fold<Duration>(
|
final total = book.tracks
|
||||||
|
.sublist(0, index)
|
||||||
|
.fold<Duration>(
|
||||||
Duration.zero,
|
Duration.zero,
|
||||||
(previousValue, element) => previousValue + element.duration,
|
(previousValue, element) => previousValue + element.duration,
|
||||||
);
|
);
|
||||||
|
|
@ -34,13 +36,10 @@ Duration sumOfTracks(BookExpanded book, int? index) {
|
||||||
/// returns the [AudioTrack] to play based on the [position] in the [book]
|
/// returns the [AudioTrack] to play based on the [position] in the [book]
|
||||||
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
|
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
|
||||||
_logger.fine('Getting track to play for position: $position');
|
_logger.fine('Getting track to play for position: $position');
|
||||||
final track = book.tracks.firstWhere(
|
final track = book.tracks.firstWhere((element) {
|
||||||
(element) {
|
return element.startOffset <= position &&
|
||||||
return element.startOffset <= position &&
|
(element.startOffset + element.duration) >= position;
|
||||||
(element.startOffset + element.duration) >= position;
|
}, orElse: () => book.tracks.last);
|
||||||
},
|
|
||||||
orElse: () => book.tracks.last,
|
|
||||||
);
|
|
||||||
_logger.fine('Track to play for position: $position is $track');
|
_logger.fine('Track to play for position: $position is $track');
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
@ -126,8 +125,12 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
ConcatenatingAudioSource(
|
ConcatenatingAudioSource(
|
||||||
useLazyPreparation: true,
|
useLazyPreparation: true,
|
||||||
children: book.tracks.map((track) {
|
children: book.tracks.map((track) {
|
||||||
final retrievedUri =
|
final retrievedUri = _getUri(
|
||||||
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
|
track,
|
||||||
|
downloadedUris,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
token: token,
|
||||||
|
);
|
||||||
_logger.fine(
|
_logger.fine(
|
||||||
'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
|
'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
|
||||||
);
|
);
|
||||||
|
|
@ -141,7 +144,8 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
.formatNotificationTitle(book),
|
.formatNotificationTitle(book),
|
||||||
album: appSettings.notificationSettings.secondaryTitle
|
album: appSettings.notificationSettings.secondaryTitle
|
||||||
.formatNotificationTitle(book),
|
.formatNotificationTitle(book),
|
||||||
artUri: artworkUri ??
|
artUri:
|
||||||
|
artworkUri ??
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
|
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
|
||||||
),
|
),
|
||||||
|
|
@ -255,12 +259,9 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
if (_book!.chapters.isEmpty) {
|
if (_book!.chapters.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return _book!.chapters.firstWhere(
|
return _book!.chapters.firstWhere((element) {
|
||||||
(element) {
|
return element.start <= positionInBook && element.end >= positionInBook;
|
||||||
return element.start <= positionInBook && element.end >= positionInBook;
|
}, orElse: () => _book!.chapters.first);
|
||||||
},
|
|
||||||
orElse: () => _book!.chapters.first,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,11 +272,9 @@ Uri _getUri(
|
||||||
required String token,
|
required String token,
|
||||||
}) {
|
}) {
|
||||||
// check if the track is in the downloadedUris
|
// check if the track is in the downloadedUris
|
||||||
final uri = downloadedUris?.firstWhereOrNull(
|
final uri = downloadedUris?.firstWhereOrNull((element) {
|
||||||
(element) {
|
return element.pathSegments.last == track.metadata?.filename;
|
||||||
return element.pathSegments.last == track.metadata?.filename;
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return uri ??
|
return uri ??
|
||||||
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
|
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
|
||||||
|
|
@ -283,17 +282,14 @@ Uri _getUri(
|
||||||
|
|
||||||
extension FormatNotificationTitle on String {
|
extension FormatNotificationTitle on String {
|
||||||
String formatNotificationTitle(BookExpanded book) {
|
String formatNotificationTitle(BookExpanded book) {
|
||||||
return replaceAllMapped(
|
return replaceAllMapped(RegExp(r'\$(\w+)'), (match) {
|
||||||
RegExp(r'\$(\w+)'),
|
final type = match.group(1);
|
||||||
(match) {
|
return NotificationTitleType.values
|
||||||
final type = match.group(1);
|
.firstWhere((element) => element.name == type)
|
||||||
return NotificationTitleType.values
|
.extractFrom(book) ??
|
||||||
.firstWhere((element) => element.name == type)
|
match.group(0) ??
|
||||||
.extractFrom(book) ??
|
'';
|
||||||
match.group(0) ??
|
});
|
||||||
'';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,23 +30,28 @@ Future<void> configurePlayer() async {
|
||||||
androidShowNotificationBadge: false,
|
androidShowNotificationBadge: false,
|
||||||
notificationConfigBuilder: (state) {
|
notificationConfigBuilder: (state) {
|
||||||
final controls = [
|
final controls = [
|
||||||
if (appSettings.notificationSettings.mediaControls
|
if (appSettings.notificationSettings.mediaControls.contains(
|
||||||
.contains(NotificationMediaControl.skipToPreviousChapter) &&
|
NotificationMediaControl.skipToPreviousChapter,
|
||||||
|
) &&
|
||||||
state.hasPrevious)
|
state.hasPrevious)
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
if (appSettings.notificationSettings.mediaControls
|
if (appSettings.notificationSettings.mediaControls.contains(
|
||||||
.contains(NotificationMediaControl.rewind))
|
NotificationMediaControl.rewind,
|
||||||
|
))
|
||||||
MediaControl.rewind,
|
MediaControl.rewind,
|
||||||
if (state.playing) MediaControl.pause else MediaControl.play,
|
if (state.playing) MediaControl.pause else MediaControl.play,
|
||||||
if (appSettings.notificationSettings.mediaControls
|
if (appSettings.notificationSettings.mediaControls.contains(
|
||||||
.contains(NotificationMediaControl.fastForward))
|
NotificationMediaControl.fastForward,
|
||||||
|
))
|
||||||
MediaControl.fastForward,
|
MediaControl.fastForward,
|
||||||
if (appSettings.notificationSettings.mediaControls
|
if (appSettings.notificationSettings.mediaControls.contains(
|
||||||
.contains(NotificationMediaControl.skipToNextChapter) &&
|
NotificationMediaControl.skipToNextChapter,
|
||||||
|
) &&
|
||||||
state.hasNext)
|
state.hasNext)
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
if (appSettings.notificationSettings.mediaControls
|
if (appSettings.notificationSettings.mediaControls.contains(
|
||||||
.contains(NotificationMediaControl.stop))
|
NotificationMediaControl.stop,
|
||||||
|
))
|
||||||
MediaControl.stop,
|
MediaControl.stop,
|
||||||
];
|
];
|
||||||
return NotificationConfig(
|
return NotificationConfig(
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ class AudiobookPlaylist {
|
||||||
this.books = const [],
|
this.books = const [],
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
subCurrentIndex = 0,
|
subCurrentIndex = 0,
|
||||||
}) : _currentIndex = currentIndex,
|
}) : _currentIndex = currentIndex,
|
||||||
_subCurrentIndex = subCurrentIndex;
|
_subCurrentIndex = subCurrentIndex;
|
||||||
|
|
||||||
// most important method, gets the audio file to play
|
// most important method, gets the audio file to play
|
||||||
// this is needed as a library item is a list of audio files
|
// this is needed as a library item is a list of audio files
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer {
|
||||||
@override
|
@override
|
||||||
core.AudiobookPlayer build() {
|
core.AudiobookPlayer build() {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final player = core.AudiobookPlayer(
|
final player = core.AudiobookPlayer(api.token!, api.baseUrl);
|
||||||
api.token!,
|
|
||||||
api.baseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.onDispose(player.dispose);
|
ref.onDispose(player.dispose);
|
||||||
_logger.finer('created simple player');
|
_logger.finer('created simple player');
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,10 @@ extension on Ref {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
Raw<ValueNotifier<double>> playerExpandProgressNotifier(Ref ref) {
|
||||||
Ref ref,
|
final ValueNotifier<double> playerExpandProgress = ValueNotifier(
|
||||||
) {
|
playerMinHeight,
|
||||||
final ValueNotifier<double> playerExpandProgress =
|
);
|
||||||
ValueNotifier(playerMinHeight);
|
|
||||||
|
|
||||||
return ref.disposeAndListenChangeNotifier(playerExpandProgress);
|
return ref.disposeAndListenChangeNotifier(playerExpandProgress);
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +45,7 @@ Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
||||||
|
|
||||||
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
|
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
double playerHeight(
|
double playerHeight(Ref ref) {
|
||||||
Ref ref,
|
|
||||||
) {
|
|
||||||
final playerExpandProgress = ref.watch(playerExpandProgressProvider);
|
final playerExpandProgress = ref.watch(playerExpandProgressProvider);
|
||||||
|
|
||||||
// on change of the playerExpandProgress invalidate
|
// on change of the playerExpandProgress invalidate
|
||||||
|
|
@ -63,9 +60,7 @@ double playerHeight(
|
||||||
final audioBookMiniplayerController = MiniplayerController();
|
final audioBookMiniplayerController = MiniplayerController();
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
bool isPlayerActive(
|
bool isPlayerActive(Ref ref) {
|
||||||
Ref ref,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
if (player.book != null) {
|
if (player.book != null) {
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,15 @@ class AudiobookPlayer extends HookConsumerWidget {
|
||||||
if (currentBook == null) {
|
if (currentBook == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
final itemBeingPlayed =
|
final itemBeingPlayed = ref.watch(
|
||||||
ref.watch(libraryItemProvider(currentBook.libraryItemId));
|
libraryItemProvider(currentBook.libraryItemId),
|
||||||
|
);
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
final imageOfItemBeingPlayed = itemBeingPlayed.value != null
|
final imageOfItemBeingPlayed = itemBeingPlayed.value != null
|
||||||
? ref.watch(
|
? ref.watch(coverImageProvider(itemBeingPlayed.value!.id))
|
||||||
coverImageProvider(itemBeingPlayed.value!.id),
|
|
||||||
)
|
|
||||||
: null;
|
: null;
|
||||||
final imgWidget = imageOfItemBeingPlayed?.value != null
|
final imgWidget = imageOfItemBeingPlayed?.value != null
|
||||||
? Image.memory(
|
? Image.memory(imageOfItemBeingPlayed!.value!, fit: BoxFit.cover)
|
||||||
imageOfItemBeingPlayed!.value!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: const BookCoverSkeleton();
|
: const BookCoverSkeleton();
|
||||||
|
|
||||||
final playPauseController = useAnimationController(
|
final playPauseController = useAnimationController(
|
||||||
|
|
@ -65,7 +61,8 @@ class AudiobookPlayer extends HookConsumerWidget {
|
||||||
themeOfLibraryItemProvider(
|
themeOfLibraryItemProvider(
|
||||||
itemBeingPlayed.value?.id,
|
itemBeingPlayed.value?.id,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
highContrast: appSettings.themeSettings.highContrast ||
|
highContrast:
|
||||||
|
appSettings.themeSettings.highContrast ||
|
||||||
MediaQuery.of(context).highContrast,
|
MediaQuery.of(context).highContrast,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -88,8 +85,9 @@ class AudiobookPlayer extends HookConsumerWidget {
|
||||||
onDragDown: (percentage) async {
|
onDragDown: (percentage) async {
|
||||||
// preferred volume
|
// preferred volume
|
||||||
// set volume to 0 when dragging down
|
// set volume to 0 when dragging down
|
||||||
await player
|
await player.setVolume(
|
||||||
.setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
|
preferredVolume * (1 - percentage.clamp(0, .75)),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
minHeight: playerMinHeight,
|
minHeight: playerMinHeight,
|
||||||
// subtract the height of notches and other system UI
|
// subtract the height of notches and other system UI
|
||||||
|
|
@ -109,17 +107,14 @@ class AudiobookPlayer extends HookConsumerWidget {
|
||||||
// also at this point the image should be at its max size and in the center of the player
|
// also at this point the image should be at its max size and in the center of the player
|
||||||
final miniplayerPercentageDeclaration =
|
final miniplayerPercentageDeclaration =
|
||||||
(maxImgSize - playerMinHeight) /
|
(maxImgSize - playerMinHeight) /
|
||||||
(playerMaxHeight - playerMinHeight);
|
(playerMaxHeight - playerMinHeight);
|
||||||
final bool isFormMiniplayer =
|
final bool isFormMiniplayer =
|
||||||
percentage < miniplayerPercentageDeclaration;
|
percentage < miniplayerPercentageDeclaration;
|
||||||
|
|
||||||
if (!isFormMiniplayer) {
|
if (!isFormMiniplayer) {
|
||||||
// this calculation needs a refactor
|
// this calculation needs a refactor
|
||||||
var percentageExpandedPlayer = percentage
|
var percentageExpandedPlayer = percentage
|
||||||
.inverseLerp(
|
.inverseLerp(miniplayerPercentageDeclaration, 1)
|
||||||
miniplayerPercentageDeclaration,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
|
|
||||||
return PlayerWhenExpanded(
|
return PlayerWhenExpanded(
|
||||||
|
|
@ -164,37 +159,33 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||||
|
|
||||||
return switch (player.processingState) {
|
return switch (player.processingState) {
|
||||||
ProcessingState.loading || ProcessingState.buffering => const Padding(
|
ProcessingState.loading || ProcessingState.buffering => const Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
ProcessingState.completed => IconButton(
|
ProcessingState.completed => IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await player.seek(const Duration(seconds: 0));
|
await player.seek(const Duration(seconds: 0));
|
||||||
await player.play();
|
await player.play();
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.replay),
|
||||||
Icons.replay,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
ProcessingState.ready => IconButton(
|
ProcessingState.ready => IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await player.togglePlayPause();
|
await player.togglePlayPause();
|
||||||
},
|
},
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
icon: AnimatedIcon(
|
icon: AnimatedIcon(
|
||||||
icon: AnimatedIcons.play_pause,
|
icon: AnimatedIcons.play_pause,
|
||||||
progress: playPauseController,
|
progress: playPauseController,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
ProcessingState.idle => const SizedBox.shrink(),
|
ProcessingState.idle => const SizedBox.shrink(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudiobookChapterProgressBar extends HookConsumerWidget {
|
class AudiobookChapterProgressBar extends HookConsumerWidget {
|
||||||
const AudiobookChapterProgressBar({
|
const AudiobookChapterProgressBar({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
const lateStart = 0.4;
|
const lateStart = 0.4;
|
||||||
const earlyEnd = 1;
|
const earlyEnd = 1;
|
||||||
final earlyPercentage = percentageExpandedPlayer
|
final earlyPercentage = percentageExpandedPlayer
|
||||||
.inverseLerp(
|
.inverseLerp(lateStart, earlyEnd)
|
||||||
lateStart,
|
|
||||||
earlyEnd,
|
|
||||||
)
|
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
||||||
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
|
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
|
||||||
|
|
@ -49,15 +46,11 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// sized box for system status bar; not needed as not full screen
|
// sized box for system status bar; not needed as not full screen
|
||||||
SizedBox(
|
SizedBox(height: MediaQuery.of(context).padding.top * earlyPercentage),
|
||||||
height: MediaQuery.of(context).padding.top * earlyPercentage,
|
|
||||||
),
|
|
||||||
|
|
||||||
// a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
|
// a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(maxHeight: 100 * earlyPercentage),
|
||||||
maxHeight: 100 * earlyPercentage,
|
|
||||||
),
|
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: earlyPercentage,
|
opacity: earlyPercentage,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -104,10 +97,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.primary
|
).colorScheme.primary.withValues(alpha: 0.1),
|
||||||
.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 32 * earlyPercentage,
|
blurRadius: 32 * earlyPercentage,
|
||||||
spreadRadius: 8 * earlyPercentage,
|
spreadRadius: 8 * earlyPercentage,
|
||||||
// offset: Offset(0, 16 * earlyPercentage),
|
// offset: Offset(0, 16 * earlyPercentage),
|
||||||
|
|
@ -170,11 +162,10 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
currentBookMetadata?.authorName ?? '',
|
currentBookMetadata?.authorName ?? '',
|
||||||
].join(' - '),
|
].join(' - '),
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
.withValues(alpha: 0.7),
|
),
|
||||||
),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,10 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
final vanishingPercentage = 1 - percentageMiniplayer;
|
final vanishingPercentage = 1 - percentageMiniplayer;
|
||||||
final progress =
|
final progress = useStream(
|
||||||
useStream(player.slowPositionStream, initialData: Duration.zero);
|
player.slowPositionStream,
|
||||||
|
initialData: Duration.zero,
|
||||||
|
);
|
||||||
|
|
||||||
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
|
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
|
||||||
|
|
||||||
|
|
@ -61,9 +63,7 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(maxWidth: maxImgSize),
|
||||||
maxWidth: maxImgSize,
|
|
||||||
),
|
|
||||||
child: imgWidget,
|
child: imgWidget,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -80,7 +80,8 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
// AutoScrollText(
|
// AutoScrollText(
|
||||||
Text(
|
Text(
|
||||||
bookMetaExpanded?.title ?? '',
|
bookMetaExpanded?.title ?? '',
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
// velocity:
|
// velocity:
|
||||||
// const Velocity(pixelsPerSecond: Offset(16, 0)),
|
// const Velocity(pixelsPerSecond: Offset(16, 0)),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
|
@ -90,11 +91,10 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
.withValues(alpha: 0.7),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -135,7 +135,8 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: (progress.data ?? Duration.zero).inSeconds /
|
value:
|
||||||
|
(progress.data ?? Duration.zero).inSeconds /
|
||||||
player.book!.duration.inSeconds,
|
player.book!.duration.inSeconds,
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@ import 'package:vaani/constants/sizes.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
|
|
||||||
class AudiobookPlayerSeekButton extends HookConsumerWidget {
|
class AudiobookPlayerSeekButton extends HookConsumerWidget {
|
||||||
const AudiobookPlayerSeekButton({
|
const AudiobookPlayerSeekButton({super.key, required this.isForward});
|
||||||
super.key,
|
|
||||||
required this.isForward,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// if true, the button seeks forward, else it seeks backwards
|
/// if true, the button seeks forward, else it seeks backwards
|
||||||
final bool isForward;
|
final bool isForward;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ import 'package:vaani/constants/sizes.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
|
|
||||||
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
||||||
const AudiobookPlayerSeekChapterButton({
|
const AudiobookPlayerSeekChapterButton({super.key, required this.isForward});
|
||||||
super.key,
|
|
||||||
required this.isForward,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// if true, the button seeks forward, else it seeks backwards
|
/// if true, the button seeks forward, else it seeks backwards
|
||||||
final bool isForward;
|
final bool isForward;
|
||||||
|
|
@ -27,9 +24,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
||||||
void seekForward() {
|
void seekForward() {
|
||||||
final index = player.book!.chapters.indexOf(player.currentChapter!);
|
final index = player.book!.chapters.indexOf(player.currentChapter!);
|
||||||
if (index < player.book!.chapters.length - 1) {
|
if (index < player.book!.chapters.length - 1) {
|
||||||
player.seek(
|
player.seek(player.book!.chapters[index + 1].start + offset);
|
||||||
player.book!.chapters[index + 1].start + offset,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
player.seek(player.currentChapter!.end);
|
player.seek(player.currentChapter!.end);
|
||||||
}
|
}
|
||||||
|
|
@ -37,8 +32,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
||||||
|
|
||||||
/// seek backward to the previous chapter or the start of the current chapter
|
/// seek backward to the previous chapter or the start of the current chapter
|
||||||
void seekBackward() {
|
void seekBackward() {
|
||||||
final currentPlayingChapterIndex =
|
final currentPlayingChapterIndex = player.book!.chapters.indexOf(
|
||||||
player.book!.chapters.indexOf(player.currentChapter!);
|
player.currentChapter!,
|
||||||
|
);
|
||||||
final chapterPosition =
|
final chapterPosition =
|
||||||
player.positionInBook - player.currentChapter!.start;
|
player.positionInBook - player.currentChapter!.start;
|
||||||
BookChapter chapterToSeekTo;
|
BookChapter chapterToSeekTo;
|
||||||
|
|
@ -49,9 +45,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
||||||
} else {
|
} else {
|
||||||
chapterToSeekTo = player.currentChapter!;
|
chapterToSeekTo = player.currentChapter!;
|
||||||
}
|
}
|
||||||
player.seek(
|
player.seek(chapterToSeekTo.start + offset);
|
||||||
chapterToSeekTo.start + offset,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ import 'package:vaani/shared/extensions/duration_format.dart'
|
||||||
import 'package:vaani/shared/hooks.dart' show useTimer;
|
import 'package:vaani/shared/hooks.dart' show useTimer;
|
||||||
|
|
||||||
class ChapterSelectionButton extends HookConsumerWidget {
|
class ChapterSelectionButton extends HookConsumerWidget {
|
||||||
const ChapterSelectionButton({
|
const ChapterSelectionButton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -49,9 +47,7 @@ class ChapterSelectionButton extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChapterSelectionModal extends HookConsumerWidget {
|
class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
const ChapterSelectionModal({
|
const ChapterSelectionModal({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -87,41 +83,40 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
child: currentBook?.chapters == null
|
child: currentBook?.chapters == null
|
||||||
? const Text('No chapters found')
|
? const Text('No chapters found')
|
||||||
: Column(
|
: Column(
|
||||||
children: currentBook!.chapters.map(
|
children: currentBook!.chapters.map((chapter) {
|
||||||
(chapter) {
|
final isCurrent = currentChapterIndex == chapter.id;
|
||||||
final isCurrent = currentChapterIndex == chapter.id;
|
final isPlayed =
|
||||||
final isPlayed = currentChapterIndex != null &&
|
currentChapterIndex != null &&
|
||||||
chapter.id < currentChapterIndex;
|
chapter.id < currentChapterIndex;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
autofocus: isCurrent,
|
autofocus: isCurrent,
|
||||||
iconColor: isPlayed && !isCurrent
|
iconColor: isPlayed && !isCurrent
|
||||||
? theme.disabledColor
|
? theme.disabledColor
|
||||||
|
: null,
|
||||||
|
title: Text(
|
||||||
|
chapter.title,
|
||||||
|
style: isPlayed && !isCurrent
|
||||||
|
? TextStyle(color: theme.disabledColor)
|
||||||
: null,
|
: null,
|
||||||
title: Text(
|
),
|
||||||
chapter.title,
|
subtitle: Text(
|
||||||
style: isPlayed && !isCurrent
|
'(${chapter.duration.smartBinaryFormat})',
|
||||||
? TextStyle(color: theme.disabledColor)
|
style: isPlayed && !isCurrent
|
||||||
: null,
|
? TextStyle(color: theme.disabledColor)
|
||||||
),
|
: null,
|
||||||
subtitle: Text(
|
),
|
||||||
'(${chapter.duration.smartBinaryFormat})',
|
trailing: isCurrent
|
||||||
style: isPlayed && !isCurrent
|
? const PlayingIndicatorIcon()
|
||||||
? TextStyle(color: theme.disabledColor)
|
: const Icon(Icons.play_arrow),
|
||||||
: null,
|
selected: isCurrent,
|
||||||
),
|
key: isCurrent ? chapterKey : null,
|
||||||
trailing: isCurrent
|
onTap: () {
|
||||||
? const PlayingIndicatorIcon()
|
Navigator.of(context).pop();
|
||||||
: const Icon(Icons.play_arrow),
|
notifier.seek(chapter.start + 90.ms);
|
||||||
selected: isCurrent,
|
notifier.play();
|
||||||
key: isCurrent ? chapterKey : null,
|
},
|
||||||
onTap: () {
|
);
|
||||||
Navigator.of(context).pop();
|
}).toList(),
|
||||||
notifier.seek(chapter.start + 90.ms);
|
|
||||||
notifier.play();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
final _logger = Logger('PlayerSpeedAdjustButton');
|
final _logger = Logger('PlayerSpeedAdjustButton');
|
||||||
|
|
||||||
class PlayerSpeedAdjustButton extends HookConsumerWidget {
|
class PlayerSpeedAdjustButton extends HookConsumerWidget {
|
||||||
const PlayerSpeedAdjustButton({
|
const PlayerSpeedAdjustButton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -35,21 +33,19 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget {
|
||||||
notifier.setSpeed(speed);
|
notifier.setSpeed(speed);
|
||||||
if (appSettings.playerSettings.configurePlayerForEveryBook) {
|
if (appSettings.playerSettings.configurePlayerForEveryBook) {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(bookSettingsProvider(bookId).notifier)
|
||||||
bookSettingsProvider(bookId).notifier,
|
|
||||||
)
|
|
||||||
.update(
|
.update(
|
||||||
bookSettings.copyWith
|
bookSettings.copyWith.playerSettings(
|
||||||
.playerSettings(preferredDefaultSpeed: speed),
|
preferredDefaultSpeed: speed,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(appSettingsProvider.notifier)
|
||||||
appSettingsProvider.notifier,
|
|
||||||
)
|
|
||||||
.update(
|
.update(
|
||||||
appSettings.copyWith
|
appSettings.copyWith.playerSettings(
|
||||||
.playerSettings(preferredDefaultSpeed: speed),
|
preferredDefaultSpeed: speed,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,11 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_animationParams =
|
_animationParams = List.generate(
|
||||||
List.generate(widget.barCount, _createRandomParams, growable: false);
|
widget.barCount,
|
||||||
|
_createRandomParams,
|
||||||
|
growable: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to generate random parameters for one bar's animation cycle
|
// Helper to generate random parameters for one bar's animation cycle
|
||||||
|
|
@ -72,10 +75,12 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
|
|
||||||
// Note: These factors represent the scale relative to the *half-height*
|
// Note: These factors represent the scale relative to the *half-height*
|
||||||
// if centerSymmetric is true, controlled by the alignment in scaleY.
|
// if centerSymmetric is true, controlled by the alignment in scaleY.
|
||||||
final targetHeightFactor1 = widget.minHeightFactor +
|
final targetHeightFactor1 =
|
||||||
|
widget.minHeightFactor +
|
||||||
_random.nextDouble() *
|
_random.nextDouble() *
|
||||||
(widget.maxHeightFactor - widget.minHeightFactor);
|
(widget.maxHeightFactor - widget.minHeightFactor);
|
||||||
final targetHeightFactor2 = widget.minHeightFactor +
|
final targetHeightFactor2 =
|
||||||
|
widget.minHeightFactor +
|
||||||
_random.nextDouble() *
|
_random.nextDouble() *
|
||||||
(widget.maxHeightFactor - widget.minHeightFactor);
|
(widget.maxHeightFactor - widget.minHeightFactor);
|
||||||
|
|
||||||
|
|
@ -95,7 +100,8 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = widget.color ??
|
final color =
|
||||||
|
widget.color ??
|
||||||
IconTheme.of(context).color ??
|
IconTheme.of(context).color ??
|
||||||
Theme.of(context).colorScheme.primary;
|
Theme.of(context).colorScheme.primary;
|
||||||
|
|
||||||
|
|
@ -110,8 +116,9 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
final double maxHeight = widget.size;
|
final double maxHeight = widget.size;
|
||||||
|
|
||||||
// Determine the alignment for scaling based on the symmetric flag
|
// Determine the alignment for scaling based on the symmetric flag
|
||||||
final Alignment scaleAlignment =
|
final Alignment scaleAlignment = widget.centerSymmetric
|
||||||
widget.centerSymmetric ? Alignment.center : Alignment.bottomCenter;
|
? Alignment.center
|
||||||
|
: Alignment.bottomCenter;
|
||||||
|
|
||||||
// Determine the cross axis alignment for the Row
|
// Determine the cross axis alignment for the Row
|
||||||
final CrossAxisAlignment rowAlignment = widget.centerSymmetric
|
final CrossAxisAlignment rowAlignment = widget.centerSymmetric
|
||||||
|
|
@ -129,47 +136,40 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
crossAxisAlignment: rowAlignment,
|
crossAxisAlignment: rowAlignment,
|
||||||
// Use spaceEvenly for better distribution, especially with center alignment
|
// Use spaceEvenly for better distribution, especially with center alignment
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: List.generate(
|
children: List.generate(widget.barCount, (index) {
|
||||||
widget.barCount,
|
final params = _animationParams[index];
|
||||||
(index) {
|
// The actual bar widget that will be animated
|
||||||
final params = _animationParams[index];
|
return Container(
|
||||||
// The actual bar widget that will be animated
|
width: barWidth,
|
||||||
return Container(
|
// Set initial height to the max potential height
|
||||||
width: barWidth,
|
// The scaleY animation will control the visible height
|
||||||
// Set initial height to the max potential height
|
height: maxHeight,
|
||||||
// The scaleY animation will control the visible height
|
decoration: BoxDecoration(
|
||||||
height: maxHeight,
|
color: color,
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(barWidth / 2),
|
||||||
color: color,
|
),
|
||||||
borderRadius: BorderRadius.circular(barWidth / 2),
|
)
|
||||||
),
|
.animate(
|
||||||
)
|
delay: params.initialDelay,
|
||||||
.animate(
|
onPlay: (controller) => controller.repeat(reverse: true),
|
||||||
delay: params.initialDelay,
|
)
|
||||||
onPlay: (controller) => controller.repeat(
|
// 1. Scale to targetHeightFactor1
|
||||||
reverse: true,
|
.scaleY(
|
||||||
),
|
begin: widget.minHeightFactor, // Scale factor starts near min
|
||||||
)
|
end: params.targetHeightFactor1,
|
||||||
// 1. Scale to targetHeightFactor1
|
duration: params.duration1,
|
||||||
.scaleY(
|
curve: Curves.easeInOutCirc,
|
||||||
begin:
|
alignment: scaleAlignment, // Apply chosen alignment
|
||||||
widget.minHeightFactor, // Scale factor starts near min
|
)
|
||||||
end: params.targetHeightFactor1,
|
// 2. Then scale to targetHeightFactor2
|
||||||
duration: params.duration1,
|
.then()
|
||||||
curve: Curves.easeInOutCirc,
|
.scaleY(
|
||||||
alignment: scaleAlignment, // Apply chosen alignment
|
end: params.targetHeightFactor2,
|
||||||
)
|
duration: params.duration2,
|
||||||
// 2. Then scale to targetHeightFactor2
|
curve: Curves.easeInOutCirc,
|
||||||
.then()
|
alignment: scaleAlignment, // Apply chosen alignment
|
||||||
.scaleY(
|
);
|
||||||
end: params.targetHeightFactor2,
|
}, growable: false),
|
||||||
duration: params.duration2,
|
|
||||||
curve: Curves.easeInOutCirc,
|
|
||||||
alignment: scaleAlignment, // Apply chosen alignment
|
|
||||||
);
|
|
||||||
},
|
|
||||||
growable: false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
const double itemExtent = 25;
|
const double itemExtent = 25;
|
||||||
|
|
||||||
class SpeedSelector extends HookConsumerWidget {
|
class SpeedSelector extends HookConsumerWidget {
|
||||||
const SpeedSelector({
|
const SpeedSelector({super.key, required this.onSpeedSelected});
|
||||||
super.key,
|
|
||||||
required this.onSpeedSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final void Function(double speed) onSpeedSelected;
|
final void Function(double speed) onSpeedSelected;
|
||||||
|
|
||||||
|
|
@ -26,34 +23,22 @@ class SpeedSelector extends HookConsumerWidget {
|
||||||
final speedState = useState(currentSpeed);
|
final speedState = useState(currentSpeed);
|
||||||
|
|
||||||
// hook the onSpeedSelected function to the state
|
// hook the onSpeedSelected function to the state
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
onSpeedSelected(speedState.value);
|
||||||
onSpeedSelected(speedState.value);
|
return null;
|
||||||
return null;
|
}, [speedState.value]);
|
||||||
},
|
|
||||||
[speedState.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
// the speed options
|
// the speed options
|
||||||
final minSpeed = min(
|
final minSpeed = min(speeds.reduce(min), playerSettings.minSpeed);
|
||||||
speeds.reduce(min),
|
final maxSpeed = max(speeds.reduce(max), playerSettings.maxSpeed);
|
||||||
playerSettings.minSpeed,
|
|
||||||
);
|
|
||||||
final maxSpeed = max(
|
|
||||||
speeds.reduce(max),
|
|
||||||
playerSettings.maxSpeed,
|
|
||||||
);
|
|
||||||
final speedIncrement = playerSettings.speedIncrement;
|
final speedIncrement = playerSettings.speedIncrement;
|
||||||
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil() + 1;
|
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil() + 1;
|
||||||
final availableSpeedsList = List.generate(
|
final availableSpeedsList = List.generate(availableSpeeds, (index) {
|
||||||
availableSpeeds,
|
// need to round to 2 decimal place to avoid floating point errors
|
||||||
(index) {
|
return double.parse(
|
||||||
// need to round to 2 decimal place to avoid floating point errors
|
(minSpeed + index * speedIncrement).toStringAsFixed(2),
|
||||||
return double.parse(
|
);
|
||||||
(minSpeed + index * speedIncrement).toStringAsFixed(2),
|
});
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final scrollController = useFixedExtentScrollController(
|
final scrollController = useFixedExtentScrollController(
|
||||||
initialItem: availableSpeedsList.indexOf(currentSpeed),
|
initialItem: availableSpeedsList.indexOf(currentSpeed),
|
||||||
|
|
@ -107,18 +92,19 @@ class SpeedSelector extends HookConsumerWidget {
|
||||||
(speed) => TextButton(
|
(speed) => TextButton(
|
||||||
style: speed == speedState.value
|
style: speed == speedState.value
|
||||||
? TextButton.styleFrom(
|
? TextButton.styleFrom(
|
||||||
backgroundColor:
|
backgroundColor: Theme.of(
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
context,
|
||||||
foregroundColor: Theme.of(context)
|
).colorScheme.primaryContainer,
|
||||||
.colorScheme
|
foregroundColor: Theme.of(
|
||||||
.onPrimaryContainer,
|
context,
|
||||||
|
).colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
// border if not selected
|
// border if not selected
|
||||||
: TextButton.styleFrom(
|
: TextButton.styleFrom(
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.primaryContainer,
|
).colorScheme.primaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
@ -195,14 +181,13 @@ class SpeedWheel extends StatelessWidget {
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemExtent: itemExtent,
|
itemExtent: itemExtent,
|
||||||
diameterRatio: 1.5, squeeze: 1.2,
|
diameterRatio: 1.5,
|
||||||
|
squeeze: 1.2,
|
||||||
// useMagnifier: true,
|
// useMagnifier: true,
|
||||||
// magnification: 1.5,
|
// magnification: 1.5,
|
||||||
physics: const FixedExtentScrollPhysics(),
|
physics: const FixedExtentScrollPhysics(),
|
||||||
children: availableSpeedsList
|
children: availableSpeedsList
|
||||||
.map(
|
.map((speed) => SpeedLine(speed: speed))
|
||||||
(speed) => SpeedLine(speed: speed),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
onSelectedItemChanged: (index) {
|
onSelectedItemChanged: (index) {
|
||||||
speedState.value = availableSpeedsList[index];
|
speedState.value = availableSpeedsList[index];
|
||||||
|
|
@ -232,10 +217,7 @@ class SpeedWheel extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpeedLine extends StatelessWidget {
|
class SpeedLine extends StatelessWidget {
|
||||||
const SpeedLine({
|
const SpeedLine({super.key, required this.speed});
|
||||||
super.key,
|
|
||||||
required this.speed,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double speed;
|
final double speed;
|
||||||
|
|
||||||
|
|
@ -250,8 +232,8 @@ class SpeedLine extends StatelessWidget {
|
||||||
width: speed % 0.5 == 0
|
width: speed % 0.5 == 0
|
||||||
? 3
|
? 3
|
||||||
: speed % 0.25 == 0
|
: speed % 0.25 == 0
|
||||||
? 2
|
? 2
|
||||||
: 0.5,
|
: 0.5,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class ShakeDetector {
|
||||||
DateTime _lastShakeTime = DateTime.now();
|
DateTime _lastShakeTime = DateTime.now();
|
||||||
|
|
||||||
final StreamController<UserAccelerometerEvent>
|
final StreamController<UserAccelerometerEvent>
|
||||||
_detectedShakeStreamController = StreamController.broadcast();
|
_detectedShakeStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
if (_accelerometerSubscription != null) {
|
if (_accelerometerSubscription != null) {
|
||||||
|
|
@ -37,26 +37,27 @@ class ShakeDetector {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_accelerometerSubscription =
|
_accelerometerSubscription =
|
||||||
userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod)
|
userAccelerometerEventStream(
|
||||||
.listen((event) {
|
samplingPeriod: _settings.samplingPeriod,
|
||||||
_logger.finest('RMS: ${event.rms}');
|
).listen((event) {
|
||||||
if (event.rms > _settings.threshold) {
|
_logger.finest('RMS: ${event.rms}');
|
||||||
_currentShakeCount++;
|
if (event.rms > _settings.threshold) {
|
||||||
|
_currentShakeCount++;
|
||||||
|
|
||||||
if (_currentShakeCount >= _settings.shakeTriggerCount &&
|
if (_currentShakeCount >= _settings.shakeTriggerCount &&
|
||||||
!isCoolDownNeeded()) {
|
!isCoolDownNeeded()) {
|
||||||
_logger.fine('Shake detected $_currentShakeCount times');
|
_logger.fine('Shake detected $_currentShakeCount times');
|
||||||
|
|
||||||
onShakeDetected?.call();
|
onShakeDetected?.call();
|
||||||
_detectedShakeStreamController.add(event);
|
_detectedShakeStreamController.add(event);
|
||||||
|
|
||||||
_lastShakeTime = DateTime.now();
|
_lastShakeTime = DateTime.now();
|
||||||
_currentShakeCount = 0;
|
_currentShakeCount = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_currentShakeCount = 0;
|
_currentShakeCount = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.fine('ShakeDetector started');
|
_logger.fine('ShakeDetector started');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,34 +59,29 @@ class ShakeDetector extends _$ShakeDetector {
|
||||||
final sleepTimer = ref.watch(sleepTimerProvider);
|
final sleepTimer = ref.watch(sleepTimerProvider);
|
||||||
if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled &&
|
if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled &&
|
||||||
sleepTimer == null) {
|
sleepTimer == null) {
|
||||||
_logger
|
_logger.config(
|
||||||
.config('No playback management is enabled and sleep timer is off, '
|
'No playback management is enabled and sleep timer is off, '
|
||||||
'so shake detection is disabled');
|
'so shake detection is disabled',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.config('Creating shake detector');
|
_logger.config('Creating shake detector');
|
||||||
final detector = core.ShakeDetector(
|
final detector = core.ShakeDetector(shakeDetectionSettings, () {
|
||||||
shakeDetectionSettings,
|
final wasActionComplete = doShakeAction(
|
||||||
() {
|
shakeDetectionSettings.shakeAction,
|
||||||
final wasActionComplete = doShakeAction(
|
ref: ref,
|
||||||
shakeDetectionSettings.shakeAction,
|
);
|
||||||
ref: ref,
|
if (wasActionComplete) {
|
||||||
);
|
shakeDetectionSettings.feedback.forEach(postShakeFeedback);
|
||||||
if (wasActionComplete) {
|
}
|
||||||
shakeDetectionSettings.feedback.forEach(postShakeFeedback);
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ref.onDispose(detector.dispose);
|
ref.onDispose(detector.dispose);
|
||||||
return detector;
|
return detector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform the shake action and return whether the action was successful
|
/// Perform the shake action and return whether the action was successful
|
||||||
bool doShakeAction(
|
bool doShakeAction(ShakeAction shakeAction, {required Ref ref}) {
|
||||||
ShakeAction shakeAction, {
|
|
||||||
required Ref ref,
|
|
||||||
}) {
|
|
||||||
final player = ref.read(simpleAudiobookPlayerProvider);
|
final player = ref.read(simpleAudiobookPlayerProvider);
|
||||||
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
|
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
|
||||||
_logger.warning('No book is loaded');
|
_logger.warning('No book is loaded');
|
||||||
|
|
@ -166,8 +161,11 @@ extension on ShakeAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isPlaybackManagementEnabled {
|
bool get isPlaybackManagementEnabled {
|
||||||
return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind}
|
return {
|
||||||
.contains(this);
|
ShakeAction.playPause,
|
||||||
|
ShakeAction.fastForward,
|
||||||
|
ShakeAction.rewind,
|
||||||
|
}.contains(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get shouldActOnSleepTimer {
|
bool get shouldActOnSleepTimer {
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,7 @@ class SleepTimer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// starts the timer with the given duration or the default duration
|
/// starts the timer with the given duration or the default duration
|
||||||
void startCountDown([
|
void startCountDown([Duration? forDuration]) {
|
||||||
Duration? forDuration,
|
|
||||||
]) {
|
|
||||||
clearCountDownTimer();
|
clearCountDownTimer();
|
||||||
duration = forDuration ?? duration;
|
duration = forDuration ?? duration;
|
||||||
timer = Timer(duration, () {
|
timer = Timer(duration, () {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||||
|
|
||||||
class SleepTimerButton extends HookConsumerWidget {
|
class SleepTimerButton extends HookConsumerWidget {
|
||||||
const SleepTimerButton({
|
const SleepTimerButton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -47,8 +45,9 @@ class SleepTimerButton extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
pendingPlayerModals--;
|
pendingPlayerModals--;
|
||||||
ref.read(sleepTimerProvider.notifier).setTimer(durationState.value);
|
ref.read(sleepTimerProvider.notifier).setTimer(durationState.value);
|
||||||
appLogger
|
appLogger.fine(
|
||||||
.fine('Sleep Timer dialog closed with ${durationState.value}');
|
'Sleep Timer dialog closed with ${durationState.value}',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
|
|
@ -57,9 +56,7 @@ class SleepTimerButton extends HookConsumerWidget {
|
||||||
Symbols.bedtime,
|
Symbols.bedtime,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
: RemainingSleepTimeDisplay(
|
: RemainingSleepTimeDisplay(timer: sleepTimer),
|
||||||
timer: sleepTimer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -67,10 +64,7 @@ class SleepTimerButton extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SleepTimerBottomSheet extends HookConsumerWidget {
|
class SleepTimerBottomSheet extends HookConsumerWidget {
|
||||||
const SleepTimerBottomSheet({
|
const SleepTimerBottomSheet({super.key, this.onDurationSelected});
|
||||||
super.key,
|
|
||||||
this.onDurationSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final void Function(Duration?)? onDurationSelected;
|
final void Function(Duration?)? onDurationSelected;
|
||||||
|
|
||||||
|
|
@ -91,8 +85,9 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
|
||||||
];
|
];
|
||||||
|
|
||||||
final scrollController = useFixedExtentScrollController(
|
final scrollController = useFixedExtentScrollController(
|
||||||
initialItem:
|
initialItem: allPossibleDurations.indexOf(
|
||||||
allPossibleDurations.indexOf(sleepTimer?.duration ?? minDuration),
|
sleepTimer?.duration ?? minDuration,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final durationState = useState<Duration>(
|
final durationState = useState<Duration>(
|
||||||
|
|
@ -100,13 +95,10 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
// useEffect to rebuild the sleep timer when the duration changes
|
// useEffect to rebuild the sleep timer when the duration changes
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
onDurationSelected?.call(durationState.value);
|
||||||
onDurationSelected?.call(durationState.value);
|
return null;
|
||||||
return null;
|
}, [durationState.value]);
|
||||||
},
|
|
||||||
[durationState.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -171,18 +163,19 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
|
||||||
(timerDuration) => TextButton(
|
(timerDuration) => TextButton(
|
||||||
style: timerDuration == durationState.value
|
style: timerDuration == durationState.value
|
||||||
? TextButton.styleFrom(
|
? TextButton.styleFrom(
|
||||||
backgroundColor:
|
backgroundColor: Theme.of(
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
context,
|
||||||
foregroundColor: Theme.of(context)
|
).colorScheme.primaryContainer,
|
||||||
.colorScheme
|
foregroundColor: Theme.of(
|
||||||
.onPrimaryContainer,
|
context,
|
||||||
|
).colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
// border if not selected
|
// border if not selected
|
||||||
: TextButton.styleFrom(
|
: TextButton.styleFrom(
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.primaryContainer,
|
).colorScheme.primaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
@ -215,10 +208,7 @@ class SleepTimerBottomSheet extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemainingSleepTimeDisplay extends HookConsumerWidget {
|
class RemainingSleepTimeDisplay extends HookConsumerWidget {
|
||||||
const RemainingSleepTimeDisplay({
|
const RemainingSleepTimeDisplay({super.key, required this.timer});
|
||||||
super.key,
|
|
||||||
required this.timer,
|
|
||||||
});
|
|
||||||
|
|
||||||
final SleepTimer timer;
|
final SleepTimer timer;
|
||||||
|
|
||||||
|
|
@ -230,17 +220,14 @@ class RemainingSleepTimeDisplay extends HookConsumerWidget {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
timer.timer == null
|
timer.timer == null
|
||||||
? timer.duration.smartBinaryFormat
|
? timer.duration.smartBinaryFormat
|
||||||
: remainingTime?.smartBinaryFormat ?? '',
|
: remainingTime?.smartBinaryFormat ?? '',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -272,8 +259,9 @@ class SleepTimerWheel extends StatelessWidget {
|
||||||
icon: const Icon(Icons.remove),
|
icon: const Icon(Icons.remove),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// animate to index - 1
|
// animate to index - 1
|
||||||
final index = availableDurations
|
final index = availableDurations.indexOf(
|
||||||
.indexOf(durationState.value ?? Duration.zero);
|
durationState.value ?? Duration.zero,
|
||||||
|
);
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
scrollController.animateToItem(
|
scrollController.animateToItem(
|
||||||
index - 1,
|
index - 1,
|
||||||
|
|
@ -289,14 +277,13 @@ class SleepTimerWheel extends StatelessWidget {
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemExtent: itemExtent,
|
itemExtent: itemExtent,
|
||||||
diameterRatio: 1.5, squeeze: 1.2,
|
diameterRatio: 1.5,
|
||||||
|
squeeze: 1.2,
|
||||||
// useMagnifier: true,
|
// useMagnifier: true,
|
||||||
// magnification: 1.5,
|
// magnification: 1.5,
|
||||||
physics: const FixedExtentScrollPhysics(),
|
physics: const FixedExtentScrollPhysics(),
|
||||||
children: availableDurations
|
children: availableDurations
|
||||||
.map(
|
.map((duration) => DurationLine(duration: duration))
|
||||||
(duration) => DurationLine(duration: duration),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
onSelectedItemChanged: (index) {
|
onSelectedItemChanged: (index) {
|
||||||
durationState.value = availableDurations[index];
|
durationState.value = availableDurations[index];
|
||||||
|
|
@ -310,8 +297,9 @@ class SleepTimerWheel extends StatelessWidget {
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// animate to index + 1
|
// animate to index + 1
|
||||||
final index = availableDurations
|
final index = availableDurations.indexOf(
|
||||||
.indexOf(durationState.value ?? Duration.zero);
|
durationState.value ?? Duration.zero,
|
||||||
|
);
|
||||||
if (index < availableDurations.length - 1) {
|
if (index < availableDurations.length - 1) {
|
||||||
scrollController.animateToItem(
|
scrollController.animateToItem(
|
||||||
index + 1,
|
index + 1,
|
||||||
|
|
@ -327,10 +315,7 @@ class SleepTimerWheel extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DurationLine extends StatelessWidget {
|
class DurationLine extends StatelessWidget {
|
||||||
const DurationLine({
|
const DurationLine({super.key, required this.duration});
|
||||||
super.key,
|
|
||||||
required this.duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
|
|
||||||
|
|
@ -345,8 +330,8 @@ class DurationLine extends StatelessWidget {
|
||||||
width: duration.inMinutes % 5 == 0
|
width: duration.inMinutes % 5 == 0
|
||||||
? 3
|
? 3
|
||||||
: duration.inMinutes % 2.5 == 0
|
: duration.inMinutes % 2.5 == 0
|
||||||
? 2
|
? 2
|
||||||
: 0.5,
|
: 0.5,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,12 @@ import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet;
|
||||||
import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer;
|
import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer;
|
||||||
|
|
||||||
class ServerManagerPage extends HookConsumerWidget {
|
class ServerManagerPage extends HookConsumerWidget {
|
||||||
const ServerManagerPage({
|
const ServerManagerPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Manage Accounts')),
|
||||||
title: const Text('Manage Accounts'),
|
|
||||||
),
|
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
|
@ -41,9 +37,7 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerManagerBody extends HookConsumerWidget {
|
class ServerManagerBody extends HookConsumerWidget {
|
||||||
const ServerManagerBody({
|
const ServerManagerBody({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -61,9 +55,7 @@ class ServerManagerBody extends HookConsumerWidget {
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('Registered Servers'),
|
||||||
'Registered Servers',
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: registeredServers.length,
|
itemCount: registeredServers.length,
|
||||||
|
|
@ -76,21 +68,17 @@ class ServerManagerBody extends HookConsumerWidget {
|
||||||
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
||||||
),
|
),
|
||||||
// children are list of users of this server
|
// children are list of users of this server
|
||||||
children: availableUsers
|
children:
|
||||||
.where(
|
availableUsers
|
||||||
(element) => element.server == registeredServer,
|
.where((element) => element.server == registeredServer)
|
||||||
)
|
.map<Widget>((e) => AvailableUserTile(user: e))
|
||||||
.map<Widget>(
|
.nonNulls
|
||||||
(e) => AvailableUserTile(user: e),
|
.toList()
|
||||||
)
|
// add buttons of delete server and add user to server at the end
|
||||||
.nonNulls
|
..addAll([
|
||||||
.toList()
|
AddUserTile(server: registeredServer),
|
||||||
|
DeleteServerTile(server: registeredServer),
|
||||||
// add buttons of delete server and add user to server at the end
|
]),
|
||||||
..addAll([
|
|
||||||
AddUserTile(server: registeredServer),
|
|
||||||
DeleteServerTile(server: registeredServer),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -111,28 +99,24 @@ class ServerManagerBody extends HookConsumerWidget {
|
||||||
final newServer = model.AudiobookShelfServer(
|
final newServer = model.AudiobookShelfServer(
|
||||||
serverUrl: makeBaseUrl(serverURIController.text),
|
serverUrl: makeBaseUrl(serverURIController.text),
|
||||||
);
|
);
|
||||||
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
ref
|
||||||
newServer,
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
);
|
.addServer(newServer);
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
apiSettings.copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeServer: newServer,
|
.updateState(
|
||||||
),
|
apiSettings.copyWith(activeServer: newServer),
|
||||||
);
|
);
|
||||||
serverURIController.clear();
|
serverURIController.clear();
|
||||||
} on ServerAlreadyExistsException catch (e) {
|
} on ServerAlreadyExistsException catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text(e.toString()),
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('Invalid URL'),
|
).showSnackBar(const SnackBar(content: Text('Invalid URL')));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -144,10 +128,7 @@ class ServerManagerBody extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteServerTile extends HookConsumerWidget {
|
class DeleteServerTile extends HookConsumerWidget {
|
||||||
const DeleteServerTile({
|
const DeleteServerTile({super.key, required this.server});
|
||||||
super.key,
|
|
||||||
required this.server,
|
|
||||||
});
|
|
||||||
|
|
||||||
final model.AudiobookShelfServer server;
|
final model.AudiobookShelfServer server;
|
||||||
|
|
||||||
|
|
@ -167,9 +148,7 @@ class DeleteServerTile extends HookConsumerWidget {
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(
|
const TextSpan(text: 'This will remove the server '),
|
||||||
text: 'This will remove the server ',
|
|
||||||
),
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: server.serverUrl.host,
|
text: server.serverUrl.host,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -194,13 +173,8 @@ class DeleteServerTile extends HookConsumerWidget {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
audiobookShelfServerProvider.notifier,
|
.removeServer(server, removeUsers: true);
|
||||||
)
|
|
||||||
.removeServer(
|
|
||||||
server,
|
|
||||||
removeUsers: true,
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Delete'),
|
child: const Text('Delete'),
|
||||||
|
|
@ -215,10 +189,7 @@ class DeleteServerTile extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddUserTile extends HookConsumerWidget {
|
class AddUserTile extends HookConsumerWidget {
|
||||||
const AddUserTile({
|
const AddUserTile({super.key, required this.server});
|
||||||
super.key,
|
|
||||||
required this.server,
|
|
||||||
});
|
|
||||||
|
|
||||||
final model.AudiobookShelfServer server;
|
final model.AudiobookShelfServer server;
|
||||||
|
|
||||||
|
|
@ -252,10 +223,12 @@ class AddUserTile extends HookConsumerWidget {
|
||||||
label: 'Switch',
|
label: 'Switch',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Switch to the new user
|
// Switch to the new user
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
ref.read(apiSettingsProvider).copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeUser: user,
|
.updateState(
|
||||||
),
|
ref
|
||||||
|
.read(apiSettingsProvider)
|
||||||
|
.copyWith(activeUser: user),
|
||||||
);
|
);
|
||||||
context.goNamed(Routes.home.name);
|
context.goNamed(Routes.home.name);
|
||||||
},
|
},
|
||||||
|
|
@ -283,10 +256,7 @@ class AddUserTile extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvailableUserTile extends HookConsumerWidget {
|
class AvailableUserTile extends HookConsumerWidget {
|
||||||
const AvailableUserTile({
|
const AvailableUserTile({super.key, required this.user});
|
||||||
super.key,
|
|
||||||
required this.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
final model.AuthenticatedUser user;
|
final model.AuthenticatedUser user;
|
||||||
|
|
||||||
|
|
@ -303,18 +273,14 @@ class AvailableUserTile extends HookConsumerWidget {
|
||||||
onTap: apiSettings.activeUser == user
|
onTap: apiSettings.activeUser == user
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
apiSettings.copyWith(
|
.read(apiSettingsProvider.notifier)
|
||||||
activeUser: user,
|
.updateState(apiSettings.copyWith(activeUser: user));
|
||||||
),
|
|
||||||
);
|
|
||||||
// pop all routes and go to the home page
|
// pop all routes and go to the home page
|
||||||
// while (context.canPop()) {
|
// while (context.canPop()) {
|
||||||
// context.pop();
|
// context.pop();
|
||||||
// }
|
// }
|
||||||
context.goNamed(
|
context.goNamed(Routes.home.name);
|
||||||
Routes.home.name,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
|
|
@ -337,9 +303,7 @@ class AvailableUserTile extends HookConsumerWidget {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(
|
const TextSpan(text: ' from this app.'),
|
||||||
text: ' from this app.',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -353,9 +317,7 @@ class AvailableUserTile extends HookConsumerWidget {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(authenticatedUsersProvider.notifier)
|
||||||
authenticatedUsersProvider.notifier,
|
|
||||||
)
|
|
||||||
.removeUser(user);
|
.removeUser(user);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:vaani/main.dart' show appLogger;
|
import 'package:vaani/main.dart' show appLogger;
|
||||||
|
|
||||||
class LibrarySwitchChip extends HookConsumerWidget {
|
class LibrarySwitchChip extends HookConsumerWidget {
|
||||||
const LibrarySwitchChip({
|
const LibrarySwitchChip({super.key, required this.libraries});
|
||||||
super.key,
|
|
||||||
required this.libraries,
|
|
||||||
});
|
|
||||||
final List<Library> libraries;
|
final List<Library> libraries;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -26,30 +23,22 @@ class LibrarySwitchChip extends HookConsumerWidget {
|
||||||
AbsIcons.getIconByName(
|
AbsIcons.getIconByName(
|
||||||
apiSettings.activeLibraryId != null
|
apiSettings.activeLibraryId != null
|
||||||
? libraries
|
? libraries
|
||||||
.firstWhere(
|
.firstWhere((lib) => lib.id == apiSettings.activeLibraryId)
|
||||||
(lib) => lib.id == apiSettings.activeLibraryId,
|
.icon
|
||||||
)
|
|
||||||
.icon
|
|
||||||
: libraries.first.icon,
|
: libraries.first.icon,
|
||||||
),
|
),
|
||||||
), // Replace with your icon
|
), // Replace with your icon
|
||||||
label: const Text('Change Library'),
|
label: const Text('Change Library'),
|
||||||
// Enable only if libraries are loaded and not empty
|
// Enable only if libraries are loaded and not empty
|
||||||
onPressed: libraries.isNotEmpty
|
onPressed: libraries.isNotEmpty
|
||||||
? () => showLibrarySwitcher(
|
? () => showLibrarySwitcher(context, ref)
|
||||||
context,
|
|
||||||
ref,
|
|
||||||
)
|
|
||||||
: null, // Disable if no libraries
|
: null, // Disable if no libraries
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper Function to Show the Switcher ---
|
// --- Helper Function to Show the Switcher ---
|
||||||
void showLibrarySwitcher(
|
void showLibrarySwitcher(BuildContext context, WidgetRef ref) {
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
) {
|
|
||||||
final content = _LibrarySelectionContent();
|
final content = _LibrarySelectionContent();
|
||||||
|
|
||||||
// --- Platform-Specific UI ---
|
// --- Platform-Specific UI ---
|
||||||
|
|
@ -209,7 +198,9 @@ class _LibrarySelectionContent extends ConsumerWidget {
|
||||||
// Get current settings state
|
// Get current settings state
|
||||||
final currentSettings = ref.read(apiSettingsProvider);
|
final currentSettings = ref.read(apiSettingsProvider);
|
||||||
// Update the active library ID
|
// Update the active library ID
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
|
.read(apiSettingsProvider.notifier)
|
||||||
|
.updateState(
|
||||||
currentSettings.copyWith(activeLibraryId: library.id),
|
currentSettings.copyWith(activeLibraryId: library.id),
|
||||||
);
|
);
|
||||||
// Close the dialog/bottom sheet
|
// Close the dialog/bottom sheet
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ import 'package:vaani/shared/widgets/not_implemented.dart';
|
||||||
import 'package:vaani/shared/widgets/vaani_logo.dart';
|
import 'package:vaani/shared/widgets/vaani_logo.dart';
|
||||||
|
|
||||||
class YouPage extends HookConsumerWidget {
|
class YouPage extends HookConsumerWidget {
|
||||||
const YouPage({
|
const YouPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -88,8 +86,9 @@ class YouPage extends HookConsumerWidget {
|
||||||
// Maybe show error details or allow retry
|
// Maybe show error details or allow retry
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content:
|
content: Text(
|
||||||
Text('Failed to load libraries: $error'),
|
'Failed to load libraries: $error',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -159,9 +158,7 @@ class YouPage extends HookConsumerWidget {
|
||||||
Theme.of(context).colorScheme.primary,
|
Theme.of(context).colorScheme.primary,
|
||||||
BlendMode.srcIn,
|
BlendMode.srcIn,
|
||||||
),
|
),
|
||||||
child: const VaaniLogo(
|
child: const VaaniLogo(size: 48),
|
||||||
size: 48,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -176,9 +173,7 @@ class YouPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserBar extends HookConsumerWidget {
|
class UserBar extends HookConsumerWidget {
|
||||||
const UserBar({
|
const UserBar({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -217,8 +212,9 @@ class UserBar extends HookConsumerWidget {
|
||||||
Text(
|
Text(
|
||||||
api.baseUrl.toString(),
|
api.baseUrl.toString(),
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
color:
|
color: themeData.colorScheme.onSurface.withValues(
|
||||||
themeData.colorScheme.onSurface.withValues(alpha: 0.6),
|
alpha: 0.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,7 @@ import 'package:flutter/material.dart';
|
||||||
class InactiveFocusScopeObserver extends StatefulWidget {
|
class InactiveFocusScopeObserver extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const InactiveFocusScopeObserver({
|
const InactiveFocusScopeObserver({super.key, required this.child});
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InactiveFocusScopeObserver> createState() =>
|
State<InactiveFocusScopeObserver> createState() =>
|
||||||
|
|
@ -39,10 +36,8 @@ class _InactiveFocusScopeObserverState
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => FocusScope(
|
Widget build(BuildContext context) =>
|
||||||
node: _focusScope,
|
FocusScope(node: _focusScope, child: widget.child);
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,7 @@ void main() async {
|
||||||
await configurePlayer();
|
await configurePlayer();
|
||||||
|
|
||||||
// run the app
|
// run the app
|
||||||
runApp(
|
runApp(const ProviderScope(child: _EagerInitialization(child: MyApp())));
|
||||||
const ProviderScope(
|
|
||||||
child: _EagerInitialization(child: MyApp()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var routerConfig = const MyAppRouter().config;
|
var routerConfig = const MyAppRouter().config;
|
||||||
|
|
@ -65,17 +61,14 @@ class MyApp extends ConsumerWidget {
|
||||||
themeSettings.highContrast || MediaQuery.of(context).highContrast;
|
themeSettings.highContrast || MediaQuery.of(context).highContrast;
|
||||||
|
|
||||||
if (shouldUseHighContrast) {
|
if (shouldUseHighContrast) {
|
||||||
lightColorScheme = lightColorScheme.copyWith(
|
lightColorScheme = lightColorScheme.copyWith(surface: Colors.white);
|
||||||
surface: Colors.white,
|
darkColorScheme = darkColorScheme.copyWith(surface: Colors.black);
|
||||||
);
|
|
||||||
darkColorScheme = darkColorScheme.copyWith(
|
|
||||||
surface: Colors.black,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeSettings.useMaterialThemeFromSystem) {
|
if (themeSettings.useMaterialThemeFromSystem) {
|
||||||
var themes =
|
var themes = ref.watch(
|
||||||
ref.watch(systemThemeProvider(highContrast: shouldUseHighContrast));
|
systemThemeProvider(highContrast: shouldUseHighContrast),
|
||||||
|
);
|
||||||
if (themes.value != null) {
|
if (themes.value != null) {
|
||||||
lightColorScheme = themes.value!.$1;
|
lightColorScheme = themes.value!.$1;
|
||||||
darkColorScheme = themes.value!.$2;
|
darkColorScheme = themes.value!.$2;
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,9 @@ class HomePage extends HookConsumerWidget {
|
||||||
// try again button
|
// try again button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
|
.read(apiSettingsProvider.notifier)
|
||||||
|
.updateState(
|
||||||
apiSettings.copyWith(activeLibraryId: null),
|
apiSettings.copyWith(activeLibraryId: null),
|
||||||
);
|
);
|
||||||
ref.invalidate(personalizedViewProvider);
|
ref.invalidate(personalizedViewProvider);
|
||||||
|
|
@ -66,24 +68,25 @@ class HomePage extends HookConsumerWidget {
|
||||||
final shelvesToDisplay = data
|
final shelvesToDisplay = data
|
||||||
// .where((element) => !element.id.contains('discover'))
|
// .where((element) => !element.id.contains('discover'))
|
||||||
.map((shelf) {
|
.map((shelf) {
|
||||||
appLogger.fine('building shelf ${shelf.label}');
|
appLogger.fine('building shelf ${shelf.label}');
|
||||||
// check if showPlayButton is enabled for the shelf
|
// check if showPlayButton is enabled for the shelf
|
||||||
// using the id of the shelf
|
// using the id of the shelf
|
||||||
final showPlayButton = switch (shelf.id) {
|
final showPlayButton = switch (shelf.id) {
|
||||||
'continue-listening' =>
|
'continue-listening' =>
|
||||||
homePageSettings.showPlayButtonOnContinueListeningShelf,
|
homePageSettings.showPlayButtonOnContinueListeningShelf,
|
||||||
'continue-series' =>
|
'continue-series' =>
|
||||||
homePageSettings.showPlayButtonOnContinueSeriesShelf,
|
homePageSettings.showPlayButtonOnContinueSeriesShelf,
|
||||||
'listen-again' =>
|
'listen-again' =>
|
||||||
homePageSettings.showPlayButtonOnListenAgainShelf,
|
homePageSettings.showPlayButtonOnListenAgainShelf,
|
||||||
_ => homePageSettings.showPlayButtonOnAllRemainingShelves,
|
_ => homePageSettings.showPlayButtonOnAllRemainingShelves,
|
||||||
};
|
};
|
||||||
return HomeShelf(
|
return HomeShelf(
|
||||||
title: shelf.label,
|
title: shelf.label,
|
||||||
shelf: shelf,
|
shelf: shelf,
|
||||||
showPlayButton: showPlayButton,
|
showPlayButton: showPlayButton,
|
||||||
);
|
);
|
||||||
}).toList();
|
})
|
||||||
|
.toList();
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
return ref.refresh(personalizedViewProvider);
|
return ref.refresh(personalizedViewProvider);
|
||||||
|
|
@ -132,10 +135,6 @@ class HomePageSkeleton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// set the library id as the active library
|
// set the library id as the active library
|
||||||
if (libraryId != null) {
|
if (libraryId != null) {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref
|
||||||
|
.read(apiSettingsProvider.notifier)
|
||||||
|
.updateState(
|
||||||
ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId),
|
ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -48,12 +50,10 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
final shelvesToDisplay = data
|
final shelvesToDisplay = data
|
||||||
// .where((element) => !element.id.contains('discover'))
|
// .where((element) => !element.id.contains('discover'))
|
||||||
.map((shelf) {
|
.map((shelf) {
|
||||||
appLogger.fine('building shelf ${shelf.label}');
|
appLogger.fine('building shelf ${shelf.label}');
|
||||||
return HomeShelf(
|
return HomeShelf(title: shelf.label, shelf: shelf);
|
||||||
title: shelf.label,
|
})
|
||||||
shelf: shelf,
|
.toList();
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
return ref.refresh(personalizedViewProvider);
|
return ref.refresh(personalizedViewProvider);
|
||||||
|
|
@ -85,10 +85,6 @@ class LibraryPageSkeleton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,8 @@
|
||||||
part of 'router.dart';
|
part of 'router.dart';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
static const home = _SimpleRoute(
|
static const home = _SimpleRoute(pathName: '', name: 'home');
|
||||||
pathName: '',
|
static const onboarding = _SimpleRoute(pathName: 'login', name: 'onboarding');
|
||||||
name: 'home',
|
|
||||||
);
|
|
||||||
static const onboarding = _SimpleRoute(
|
|
||||||
pathName: 'login',
|
|
||||||
name: 'onboarding',
|
|
||||||
);
|
|
||||||
static const library = _SimpleRoute(
|
static const library = _SimpleRoute(
|
||||||
pathName: 'library',
|
pathName: 'library',
|
||||||
pathParamName: 'libraryId',
|
pathParamName: 'libraryId',
|
||||||
|
|
@ -23,10 +17,7 @@ class Routes {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Local settings
|
// Local settings
|
||||||
static const settings = _SimpleRoute(
|
static const settings = _SimpleRoute(pathName: 'config', name: 'settings');
|
||||||
pathName: 'config',
|
|
||||||
name: 'settings',
|
|
||||||
);
|
|
||||||
static const themeSettings = _SimpleRoute(
|
static const themeSettings = _SimpleRoute(
|
||||||
pathName: 'theme',
|
pathName: 'theme',
|
||||||
name: 'themeSettings',
|
name: 'themeSettings',
|
||||||
|
|
@ -64,10 +55,7 @@ class Routes {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
// parentRoute: library,
|
// parentRoute: library,
|
||||||
);
|
);
|
||||||
static const explore = _SimpleRoute(
|
static const explore = _SimpleRoute(pathName: 'explore', name: 'explore');
|
||||||
pathName: 'explore',
|
|
||||||
name: 'explore',
|
|
||||||
);
|
|
||||||
|
|
||||||
// downloads
|
// downloads
|
||||||
static const downloads = _SimpleRoute(
|
static const downloads = _SimpleRoute(
|
||||||
|
|
@ -83,10 +71,7 @@ class Routes {
|
||||||
);
|
);
|
||||||
|
|
||||||
// you page for the user
|
// you page for the user
|
||||||
static const you = _SimpleRoute(
|
static const you = _SimpleRoute(pathName: 'you', name: 'you');
|
||||||
pathName: 'you',
|
|
||||||
name: 'you',
|
|
||||||
);
|
|
||||||
|
|
||||||
// user management
|
// user management
|
||||||
static const userManagement = _SimpleRoute(
|
static const userManagement = _SimpleRoute(
|
||||||
|
|
@ -102,10 +87,7 @@ class Routes {
|
||||||
);
|
);
|
||||||
|
|
||||||
// logs page
|
// logs page
|
||||||
static const logs = _SimpleRoute(
|
static const logs = _SimpleRoute(pathName: 'logs', name: 'logs');
|
||||||
pathName: 'logs',
|
|
||||||
name: 'logs',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// a class to store path
|
// a class to store path
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ import 'transitions/slide.dart';
|
||||||
|
|
||||||
part 'constants.dart';
|
part 'constants.dart';
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> rootNavigatorKey =
|
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(
|
||||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
debugLabel: 'root',
|
||||||
|
);
|
||||||
final GlobalKey<NavigatorState> sectionHomeNavigatorKey =
|
final GlobalKey<NavigatorState> sectionHomeNavigatorKey =
|
||||||
GlobalKey<NavigatorState>(debugLabel: 'HomeNavigator');
|
GlobalKey<NavigatorState>(debugLabel: 'HomeNavigator');
|
||||||
|
|
||||||
|
|
@ -35,34 +36,35 @@ class MyAppRouter {
|
||||||
const MyAppRouter();
|
const MyAppRouter();
|
||||||
|
|
||||||
GoRouter get config => GoRouter(
|
GoRouter get config => GoRouter(
|
||||||
initialLocation: Routes.home.localPath,
|
initialLocation: Routes.home.localPath,
|
||||||
debugLogDiagnostics: true,
|
debugLogDiagnostics: true,
|
||||||
|
routes: [
|
||||||
|
// sign in page
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.onboarding.localPath,
|
||||||
|
name: Routes.onboarding.name,
|
||||||
|
builder: (context, state) => const OnboardingSinglePage(),
|
||||||
routes: [
|
routes: [
|
||||||
// sign in page
|
// open id callback
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.onboarding.localPath,
|
path: Routes.openIDCallback.pathName,
|
||||||
name: Routes.onboarding.name,
|
name: Routes.openIDCallback.name,
|
||||||
builder: (context, state) => const OnboardingSinglePage(),
|
|
||||||
routes: [
|
|
||||||
// open id callback
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.openIDCallback.pathName,
|
|
||||||
name: Routes.openIDCallback.name,
|
|
||||||
pageBuilder: handleCallback,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// callback for open id
|
|
||||||
// need to duplicate because of https://github.com/flutter/flutter/issues/100624
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.openIDCallback.localPath,
|
|
||||||
// name: Routes.openIDCallback.name,
|
|
||||||
// builder: handleCallback,
|
|
||||||
pageBuilder: handleCallback,
|
pageBuilder: handleCallback,
|
||||||
),
|
),
|
||||||
// The main app shell
|
],
|
||||||
StatefulShellRoute.indexedStack(
|
),
|
||||||
builder: (
|
// callback for open id
|
||||||
|
// need to duplicate because of https://github.com/flutter/flutter/issues/100624
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.openIDCallback.localPath,
|
||||||
|
// name: Routes.openIDCallback.name,
|
||||||
|
// builder: handleCallback,
|
||||||
|
pageBuilder: handleCallback,
|
||||||
|
),
|
||||||
|
// The main app shell
|
||||||
|
StatefulShellRoute.indexedStack(
|
||||||
|
builder:
|
||||||
|
(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
GoRouterState state,
|
GoRouterState state,
|
||||||
StatefulNavigationShell navigationShell,
|
StatefulNavigationShell navigationShell,
|
||||||
|
|
@ -73,188 +75,187 @@ class MyAppRouter {
|
||||||
// branches in a stateful way.
|
// branches in a stateful way.
|
||||||
return ScaffoldWithNavBar(navigationShell: navigationShell);
|
return ScaffoldWithNavBar(navigationShell: navigationShell);
|
||||||
},
|
},
|
||||||
branches: <StatefulShellBranch>[
|
branches: <StatefulShellBranch>[
|
||||||
// The route branch for the first tab of the bottom navigation bar.
|
// The route branch for the first tab of the bottom navigation bar.
|
||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
navigatorKey: sectionHomeNavigatorKey,
|
navigatorKey: sectionHomeNavigatorKey,
|
||||||
routes: <RouteBase>[
|
routes: <RouteBase>[
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.home.localPath,
|
path: Routes.home.localPath,
|
||||||
name: Routes.home.name,
|
name: Routes.home.name,
|
||||||
// builder: (context, state) => const HomePage(),
|
// builder: (context, state) => const HomePage(),
|
||||||
pageBuilder: defaultPageBuilder(const HomePage()),
|
pageBuilder: defaultPageBuilder(const HomePage()),
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.libraryItem.localPath,
|
|
||||||
name: Routes.libraryItem.name,
|
|
||||||
// builder: (context, state) {
|
|
||||||
// final itemId = state
|
|
||||||
// .pathParameters[Routes.libraryItem.pathParamName]!;
|
|
||||||
// return LibraryItemPage(
|
|
||||||
// itemId: itemId, extra: state.extra);
|
|
||||||
// },
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
final itemId = state
|
|
||||||
.pathParameters[Routes.libraryItem.pathParamName]!;
|
|
||||||
final child =
|
|
||||||
LibraryItemPage(itemId: itemId, extra: state.extra);
|
|
||||||
return buildPageWithDefaultTransition(
|
|
||||||
context: context,
|
|
||||||
state: state,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// downloads page
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.downloads.localPath,
|
|
||||||
name: Routes.downloads.name,
|
|
||||||
pageBuilder: defaultPageBuilder(const DownloadsPage()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
// Library page
|
path: Routes.libraryItem.localPath,
|
||||||
StatefulShellBranch(
|
name: Routes.libraryItem.name,
|
||||||
routes: <RouteBase>[
|
// builder: (context, state) {
|
||||||
GoRoute(
|
// final itemId = state
|
||||||
path: Routes.libraryBrowser.localPath,
|
// .pathParameters[Routes.libraryItem.pathParamName]!;
|
||||||
name: Routes.libraryBrowser.name,
|
// return LibraryItemPage(
|
||||||
pageBuilder: defaultPageBuilder(const LibraryBrowserPage()),
|
// itemId: itemId, extra: state.extra);
|
||||||
),
|
// },
|
||||||
],
|
pageBuilder: (context, state) {
|
||||||
|
final itemId =
|
||||||
|
state.pathParameters[Routes.libraryItem.pathParamName]!;
|
||||||
|
final child = LibraryItemPage(
|
||||||
|
itemId: itemId,
|
||||||
|
extra: state.extra,
|
||||||
|
);
|
||||||
|
return buildPageWithDefaultTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
// search/explore page
|
// downloads page
|
||||||
StatefulShellBranch(
|
GoRoute(
|
||||||
routes: <RouteBase>[
|
path: Routes.downloads.localPath,
|
||||||
GoRoute(
|
name: Routes.downloads.name,
|
||||||
path: Routes.explore.localPath,
|
pageBuilder: defaultPageBuilder(const DownloadsPage()),
|
||||||
name: Routes.explore.name,
|
|
||||||
// builder: (context, state) => const ExplorePage(),
|
|
||||||
pageBuilder: defaultPageBuilder(const ExplorePage()),
|
|
||||||
),
|
|
||||||
// search page
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.search.localPath,
|
|
||||||
name: Routes.search.name,
|
|
||||||
// builder: (context, state) {
|
|
||||||
// final libraryId = state
|
|
||||||
// .pathParameters[Routes.library.pathParamName]!;
|
|
||||||
// return LibrarySearchPage(
|
|
||||||
// libraryId: libraryId,
|
|
||||||
// extra: state.extra,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
final queryParam = state.uri.queryParameters['q']!;
|
|
||||||
final category = state.uri.queryParameters['category'];
|
|
||||||
final child = SearchResultPage(
|
|
||||||
extra: state.extra,
|
|
||||||
query: queryParam,
|
|
||||||
category: category != null
|
|
||||||
? SearchResultCategory.values.firstWhere(
|
|
||||||
(e) => e.toString().split('.').last == category,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
return buildPageWithDefaultTransition(
|
|
||||||
context: context,
|
|
||||||
state: state,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// you page
|
|
||||||
StatefulShellBranch(
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.you.localPath,
|
|
||||||
name: Routes.you.name,
|
|
||||||
pageBuilder: defaultPageBuilder(const YouPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.settings.localPath,
|
|
||||||
name: Routes.settings.name,
|
|
||||||
// builder: (context, state) => const AppSettingsPage(),
|
|
||||||
pageBuilder: defaultPageBuilder(const AppSettingsPage()),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.themeSettings.pathName,
|
|
||||||
name: Routes.themeSettings.name,
|
|
||||||
pageBuilder: defaultPageBuilder(
|
|
||||||
const ThemeSettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.autoSleepTimerSettings.pathName,
|
|
||||||
name: Routes.autoSleepTimerSettings.name,
|
|
||||||
pageBuilder: defaultPageBuilder(
|
|
||||||
const AutoSleepTimerSettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.notificationSettings.pathName,
|
|
||||||
name: Routes.notificationSettings.name,
|
|
||||||
pageBuilder: defaultPageBuilder(
|
|
||||||
const NotificationSettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.playerSettings.pathName,
|
|
||||||
name: Routes.playerSettings.name,
|
|
||||||
pageBuilder:
|
|
||||||
defaultPageBuilder(const PlayerSettingsPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.shakeDetectorSettings.pathName,
|
|
||||||
name: Routes.shakeDetectorSettings.name,
|
|
||||||
pageBuilder: defaultPageBuilder(
|
|
||||||
const ShakeDetectorSettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.homePageSettings.pathName,
|
|
||||||
name: Routes.homePageSettings.name,
|
|
||||||
pageBuilder: defaultPageBuilder(
|
|
||||||
const HomePageSettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: Routes.userManagement.localPath,
|
|
||||||
name: Routes.userManagement.name,
|
|
||||||
// builder: (context, state) => const UserManagementPage(),
|
|
||||||
pageBuilder: defaultPageBuilder(const ServerManagerPage()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// loggers page
|
// Library page
|
||||||
GoRoute(
|
StatefulShellBranch(
|
||||||
path: Routes.logs.localPath,
|
routes: <RouteBase>[
|
||||||
name: Routes.logs.name,
|
GoRoute(
|
||||||
// builder: (context, state) => const LogsPage(),
|
path: Routes.libraryBrowser.localPath,
|
||||||
pageBuilder: defaultPageBuilder(const LogsPage()),
|
name: Routes.libraryBrowser.name,
|
||||||
|
pageBuilder: defaultPageBuilder(const LibraryBrowserPage()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// search/explore page
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.explore.localPath,
|
||||||
|
name: Routes.explore.name,
|
||||||
|
// builder: (context, state) => const ExplorePage(),
|
||||||
|
pageBuilder: defaultPageBuilder(const ExplorePage()),
|
||||||
|
),
|
||||||
|
// search page
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.search.localPath,
|
||||||
|
name: Routes.search.name,
|
||||||
|
// builder: (context, state) {
|
||||||
|
// final libraryId = state
|
||||||
|
// .pathParameters[Routes.library.pathParamName]!;
|
||||||
|
// return LibrarySearchPage(
|
||||||
|
// libraryId: libraryId,
|
||||||
|
// extra: state.extra,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final queryParam = state.uri.queryParameters['q']!;
|
||||||
|
final category = state.uri.queryParameters['category'];
|
||||||
|
final child = SearchResultPage(
|
||||||
|
extra: state.extra,
|
||||||
|
query: queryParam,
|
||||||
|
category: category != null
|
||||||
|
? SearchResultCategory.values.firstWhere(
|
||||||
|
(e) => e.toString().split('.').last == category,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
return buildPageWithDefaultTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// you page
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.you.localPath,
|
||||||
|
name: Routes.you.name,
|
||||||
|
pageBuilder: defaultPageBuilder(const YouPage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.settings.localPath,
|
||||||
|
name: Routes.settings.name,
|
||||||
|
// builder: (context, state) => const AppSettingsPage(),
|
||||||
|
pageBuilder: defaultPageBuilder(const AppSettingsPage()),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.themeSettings.pathName,
|
||||||
|
name: Routes.themeSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(const ThemeSettingsPage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.autoSleepTimerSettings.pathName,
|
||||||
|
name: Routes.autoSleepTimerSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(
|
||||||
|
const AutoSleepTimerSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.notificationSettings.pathName,
|
||||||
|
name: Routes.notificationSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(
|
||||||
|
const NotificationSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.playerSettings.pathName,
|
||||||
|
name: Routes.playerSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(const PlayerSettingsPage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.shakeDetectorSettings.pathName,
|
||||||
|
name: Routes.shakeDetectorSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(
|
||||||
|
const ShakeDetectorSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.homePageSettings.pathName,
|
||||||
|
name: Routes.homePageSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(
|
||||||
|
const HomePageSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.userManagement.localPath,
|
||||||
|
name: Routes.userManagement.name,
|
||||||
|
// builder: (context, state) => const UserManagementPage(),
|
||||||
|
pageBuilder: defaultPageBuilder(const ServerManagerPage()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
|
|
||||||
Page handleCallback(
|
// loggers page
|
||||||
BuildContext context,
|
GoRoute(
|
||||||
GoRouterState state,
|
path: Routes.logs.localPath,
|
||||||
) {
|
name: Routes.logs.name,
|
||||||
|
// builder: (context, state) => const LogsPage(),
|
||||||
|
pageBuilder: defaultPageBuilder(const LogsPage()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Page handleCallback(BuildContext context, GoRouterState state) {
|
||||||
// extract the code and state from the uri
|
// extract the code and state from the uri
|
||||||
final code = state.uri.queryParameters['code'];
|
final code = state.uri.queryParameters['code'];
|
||||||
final stateParam = state.uri.queryParameters['state'];
|
final stateParam = state.uri.queryParameters['state'];
|
||||||
appLogger.fine('deep linking callback: code: $code, state: $stateParam');
|
appLogger.fine('deep linking callback: code: $code, state: $stateParam');
|
||||||
|
|
||||||
var callbackPage =
|
var callbackPage = CallbackPage(
|
||||||
CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam));
|
code: code,
|
||||||
|
state: stateParam,
|
||||||
|
key: ValueKey(stateParam),
|
||||||
|
);
|
||||||
return buildPageWithDefaultTransition(
|
return buildPageWithDefaultTransition(
|
||||||
context: context,
|
context: context,
|
||||||
state: state,
|
state: state,
|
||||||
|
|
|
||||||
|
|
@ -33,29 +33,26 @@ CustomTransitionPage buildPageWithDefaultTransition<T>({
|
||||||
child: child,
|
child: child,
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
FadeTransition(
|
FadeTransition(
|
||||||
opacity: animation,
|
opacity: animation,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: animation.drive(
|
position: animation.drive(
|
||||||
Tween(
|
Tween(
|
||||||
begin: const Offset(0, 1.50),
|
begin: const Offset(0, 1.50),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).chain(
|
).chain(CurveTween(curve: Curves.easeOut)),
|
||||||
CurveTween(curve: Curves.easeOut),
|
),
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Page<dynamic> Function(BuildContext, GoRouterState) defaultPageBuilder<T>(
|
Page<dynamic> Function(BuildContext, GoRouterState) defaultPageBuilder<T>(
|
||||||
Widget child,
|
Widget child,
|
||||||
) =>
|
) => (BuildContext context, GoRouterState state) {
|
||||||
(BuildContext context, GoRouterState state) {
|
return buildPageWithDefaultTransition<T>(
|
||||||
return buildPageWithDefaultTransition<T>(
|
context: context,
|
||||||
context: context,
|
state: state,
|
||||||
state: state,
|
child: child,
|
||||||
child: child,
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ model.AppSettings loadOrCreateAppSettings() {
|
||||||
settings = _box.getAt(0);
|
settings = _box.getAt(0);
|
||||||
_logger.fine('found settings in box: $settings');
|
_logger.fine('found settings in box: $settings');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.warning('error reading settings from box: $e'
|
_logger.warning(
|
||||||
'\nclearing box');
|
'error reading settings from box: $e'
|
||||||
|
'\nclearing box',
|
||||||
|
);
|
||||||
_box.clear();
|
_box.clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,19 @@ Future<String> deviceName(Ref ref) async {
|
||||||
|
|
||||||
// try different keys to get the device name
|
// try different keys to get the device name
|
||||||
return
|
return
|
||||||
// android
|
// android
|
||||||
data['product'] ??
|
data['product'] ??
|
||||||
// ios
|
// ios
|
||||||
data['name'] ??
|
data['name'] ??
|
||||||
// linux
|
// linux
|
||||||
data['name'] ??
|
data['name'] ??
|
||||||
// windows
|
// windows
|
||||||
data['computerName'] ??
|
data['computerName'] ??
|
||||||
// macos
|
// macos
|
||||||
data['model'] ??
|
data['model'] ??
|
||||||
// web
|
// web
|
||||||
data['browserName'] ??
|
data['browserName'] ??
|
||||||
'Unknown name';
|
'Unknown name';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
|
|
@ -33,19 +33,19 @@ Future<String> deviceModel(Ref ref) async {
|
||||||
|
|
||||||
// try different keys to get the device model
|
// try different keys to get the device model
|
||||||
return
|
return
|
||||||
// android, eg: Google Pixel 4
|
// android, eg: Google Pixel 4
|
||||||
|
data['model'] ??
|
||||||
|
// ios, eg: iPhone 12 Pro
|
||||||
|
data['name'] ??
|
||||||
|
// linux, eg: Linux Mint 20.1
|
||||||
|
data['name'] ??
|
||||||
|
// windows, eg: Surface Pro 7
|
||||||
|
data['productId'] ??
|
||||||
|
// macos, eg: MacBook Pro (13-inch, M1, 2020)
|
||||||
data['model'] ??
|
data['model'] ??
|
||||||
// ios, eg: iPhone 12 Pro
|
// web, eg: Chrome 87.0.4280.88
|
||||||
data['name'] ??
|
data['browserName'] ??
|
||||||
// linux, eg: Linux Mint 20.1
|
'Unknown model';
|
||||||
data['name'] ??
|
|
||||||
// windows, eg: Surface Pro 7
|
|
||||||
data['productId'] ??
|
|
||||||
// macos, eg: MacBook Pro (13-inch, M1, 2020)
|
|
||||||
data['model'] ??
|
|
||||||
// web, eg: Chrome 87.0.4280.88
|
|
||||||
data['browserName'] ??
|
|
||||||
'Unknown model';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
|
|
@ -54,19 +54,19 @@ Future<String> deviceSdkVersion(Ref ref) async {
|
||||||
|
|
||||||
// try different keys to get the device sdk version
|
// try different keys to get the device sdk version
|
||||||
return
|
return
|
||||||
// android, eg: 30
|
// android, eg: 30
|
||||||
data['version.sdkInt']?.toString() ??
|
data['version.sdkInt']?.toString() ??
|
||||||
// ios, eg: 14.4
|
// ios, eg: 14.4
|
||||||
data['systemVersion'] ??
|
data['systemVersion'] ??
|
||||||
// linux, eg: 5.4.0-66-generic
|
// linux, eg: 5.4.0-66-generic
|
||||||
data['version'] ??
|
data['version'] ??
|
||||||
// windows, eg: 10.0.19042
|
// windows, eg: 10.0.19042
|
||||||
data['displayVersion'] ??
|
data['displayVersion'] ??
|
||||||
// macos, eg: 11.2.1
|
// macos, eg: 11.2.1
|
||||||
data['osRelease'] ??
|
data['osRelease'] ??
|
||||||
// web, eg: 87.0.4280.88
|
// web, eg: 87.0.4280.88
|
||||||
data['appVersion'] ??
|
data['appVersion'] ??
|
||||||
'Unknown sdk version';
|
'Unknown sdk version';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
|
|
@ -75,19 +75,19 @@ Future<String> deviceManufacturer(Ref ref) async {
|
||||||
|
|
||||||
// try different keys to get the device manufacturer
|
// try different keys to get the device manufacturer
|
||||||
return
|
return
|
||||||
// android, eg: Google
|
// android, eg: Google
|
||||||
|
data['manufacturer'] ??
|
||||||
|
// ios, eg: Apple
|
||||||
data['manufacturer'] ??
|
data['manufacturer'] ??
|
||||||
// ios, eg: Apple
|
// linux, eg: Linux
|
||||||
data['manufacturer'] ??
|
data['idLike'] ??
|
||||||
// linux, eg: Linux
|
// windows, eg: Microsoft
|
||||||
data['idLike'] ??
|
data['productName'] ??
|
||||||
// windows, eg: Microsoft
|
// macos, eg: Apple
|
||||||
data['productName'] ??
|
data['manufacturer'] ??
|
||||||
// macos, eg: Apple
|
// web, eg: Google Inc.
|
||||||
data['manufacturer'] ??
|
data['vendor'] ??
|
||||||
// web, eg: Google Inc.
|
'Unknown manufacturer';
|
||||||
data['vendor'] ??
|
|
||||||
'Unknown manufacturer';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// copied from https://pub.dev/packages/device_info_plus/example
|
// copied from https://pub.dev/packages/device_info_plus/example
|
||||||
|
|
@ -234,25 +234,28 @@ Future<Map<String, dynamic>> _getDeviceData(
|
||||||
deviceData = _readWebBrowserInfo(await deviceInfoPlugin.webBrowserInfo);
|
deviceData = _readWebBrowserInfo(await deviceInfoPlugin.webBrowserInfo);
|
||||||
} else {
|
} else {
|
||||||
deviceData = switch (defaultTargetPlatform) {
|
deviceData = switch (defaultTargetPlatform) {
|
||||||
TargetPlatform.android =>
|
TargetPlatform.android => _readAndroidBuildData(
|
||||||
_readAndroidBuildData(await deviceInfoPlugin.androidInfo),
|
await deviceInfoPlugin.androidInfo,
|
||||||
TargetPlatform.iOS =>
|
),
|
||||||
_readIosDeviceInfo(await deviceInfoPlugin.iosInfo),
|
TargetPlatform.iOS => _readIosDeviceInfo(
|
||||||
TargetPlatform.linux =>
|
await deviceInfoPlugin.iosInfo,
|
||||||
_readLinuxDeviceInfo(await deviceInfoPlugin.linuxInfo),
|
),
|
||||||
TargetPlatform.windows =>
|
TargetPlatform.linux => _readLinuxDeviceInfo(
|
||||||
_readWindowsDeviceInfo(await deviceInfoPlugin.windowsInfo),
|
await deviceInfoPlugin.linuxInfo,
|
||||||
TargetPlatform.macOS =>
|
),
|
||||||
_readMacOsDeviceInfo(await deviceInfoPlugin.macOsInfo),
|
TargetPlatform.windows => _readWindowsDeviceInfo(
|
||||||
|
await deviceInfoPlugin.windowsInfo,
|
||||||
|
),
|
||||||
|
TargetPlatform.macOS => _readMacOsDeviceInfo(
|
||||||
|
await deviceInfoPlugin.macOsInfo,
|
||||||
|
),
|
||||||
TargetPlatform.fuchsia => <String, dynamic>{
|
TargetPlatform.fuchsia => <String, dynamic>{
|
||||||
errorKey: 'Fuchsia platform isn\'t supported',
|
errorKey: 'Fuchsia platform isn\'t supported',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} on PlatformException {
|
} on PlatformException {
|
||||||
deviceData = <String, dynamic>{
|
deviceData = <String, dynamic>{errorKey: 'Failed to get platform version.'};
|
||||||
errorKey: 'Failed to get platform version.',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return deviceData;
|
return deviceData;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart';
|
import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart';
|
||||||
|
|
||||||
class AppSettingsPage extends HookConsumerWidget {
|
class AppSettingsPage extends HookConsumerWidget {
|
||||||
const AppSettingsPage({
|
const AppSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -33,17 +31,12 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text('General', style: Theme.of(context).textTheme.titleLarge),
|
||||||
'General',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: const Text('Player Settings'),
|
title: const Text('Player Settings'),
|
||||||
leading: const Icon(Icons.play_arrow),
|
leading: const Icon(Icons.play_arrow),
|
||||||
description: const Text(
|
description: const Text('Customize the player settings'),
|
||||||
'Customize the player settings',
|
|
||||||
),
|
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
context.pushNamed(Routes.playerSettings.name);
|
context.pushNamed(Routes.playerSettings.name);
|
||||||
},
|
},
|
||||||
|
|
@ -61,7 +54,9 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
value: sleepTimerSettings.autoTurnOnTimer,
|
value: sleepTimerSettings.autoTurnOnTimer,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.sleepTimerSettings(
|
appSettings.copyWith.sleepTimerSettings(
|
||||||
autoTurnOnTimer: value,
|
autoTurnOnTimer: value,
|
||||||
),
|
),
|
||||||
|
|
@ -71,15 +66,15 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
NavigationWithSwitchTile(
|
NavigationWithSwitchTile(
|
||||||
title: const Text('Shake Detector'),
|
title: const Text('Shake Detector'),
|
||||||
leading: const Icon(Icons.vibration),
|
leading: const Icon(Icons.vibration),
|
||||||
description: const Text(
|
description: const Text('Customize the shake detector settings'),
|
||||||
'Customize the shake detector settings',
|
|
||||||
),
|
|
||||||
value: appSettings.shakeDetectionSettings.isEnabled,
|
value: appSettings.shakeDetectionSettings.isEnabled,
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
context.pushNamed(Routes.shakeDetectorSettings.name);
|
context.pushNamed(Routes.shakeDetectorSettings.name);
|
||||||
},
|
},
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.shakeDetectionSettings(
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
isEnabled: value,
|
isEnabled: value,
|
||||||
),
|
),
|
||||||
|
|
@ -103,9 +98,7 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
SettingsTile.navigation(
|
SettingsTile.navigation(
|
||||||
leading: const Icon(Icons.color_lens),
|
leading: const Icon(Icons.color_lens),
|
||||||
title: const Text('Theme Settings'),
|
title: const Text('Theme Settings'),
|
||||||
description: const Text(
|
description: const Text('Customize the app theme'),
|
||||||
'Customize the app theme',
|
|
||||||
),
|
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
context.pushNamed(Routes.themeSettings.name);
|
context.pushNamed(Routes.themeSettings.name);
|
||||||
},
|
},
|
||||||
|
|
@ -123,9 +116,7 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
SettingsTile.navigation(
|
SettingsTile.navigation(
|
||||||
leading: const Icon(Icons.home_filled),
|
leading: const Icon(Icons.home_filled),
|
||||||
title: const Text('Home Page Settings'),
|
title: const Text('Home Page Settings'),
|
||||||
description: const Text(
|
description: const Text('Customize the home page'),
|
||||||
'Customize the home page',
|
|
||||||
),
|
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
context.pushNamed(Routes.homePageSettings.name);
|
context.pushNamed(Routes.homePageSettings.name);
|
||||||
},
|
},
|
||||||
|
|
@ -147,21 +138,15 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: const Text('Copy to Clipboard'),
|
title: const Text('Copy to Clipboard'),
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
description: const Text(
|
description: const Text('Copy the app settings to the clipboard'),
|
||||||
'Copy the app settings to the clipboard',
|
|
||||||
),
|
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
// copy to clipboard
|
// copy to clipboard
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(text: jsonEncode(appSettings.toJson())),
|
||||||
text: jsonEncode(appSettings.toJson()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
// show toast
|
// show toast
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text('Settings copied to clipboard')),
|
||||||
content: Text('Settings copied to clipboard'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -231,9 +216,7 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RestoreDialogue extends HookConsumerWidget {
|
class RestoreDialogue extends HookConsumerWidget {
|
||||||
const RestoreDialogue({
|
const RestoreDialogue({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -264,9 +247,7 @@ class RestoreDialogue extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// try to decode the backup
|
// try to decode the backup
|
||||||
settings.value = model.AppSettings.fromJson(
|
settings.value = model.AppSettings.fromJson(jsonDecode(value));
|
||||||
jsonDecode(value),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Invalid backup';
|
return 'Invalid backup';
|
||||||
}
|
}
|
||||||
|
|
@ -280,27 +261,21 @@ class RestoreDialogue extends HookConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState!.validate()) {
|
||||||
if (settings.value == null) {
|
if (settings.value == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('Invalid backup'),
|
).showSnackBar(const SnackBar(content: Text('Invalid backup')));
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ref.read(appSettingsProvider.notifier).update(settings.value!);
|
ref.read(appSettingsProvider.notifier).update(settings.value!);
|
||||||
settingsInputController.clear();
|
settingsInputController.clear();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text('Settings restored')),
|
||||||
content: Text('Settings restored'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('Invalid backup'),
|
).showSnackBar(const SnackBar(content: Text('Invalid backup')));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Restore'),
|
child: const Text('Restore'),
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,15 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/shared/extensions/time_of_day.dart';
|
import 'package:vaani/shared/extensions/time_of_day.dart';
|
||||||
|
|
||||||
class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
const AutoSleepTimerSettingsPage({
|
const AutoSleepTimerSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettings = ref.watch(appSettingsProvider);
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
final sleepTimerSettings = appSettings.sleepTimerSettings;
|
final sleepTimerSettings = appSettings.sleepTimerSettings;
|
||||||
|
|
||||||
var enabled = sleepTimerSettings.autoTurnOnTimer &&
|
var enabled =
|
||||||
|
sleepTimerSettings.autoTurnOnTimer &&
|
||||||
!sleepTimerSettings.alwaysAutoTurnOnTimer;
|
!sleepTimerSettings.alwaysAutoTurnOnTimer;
|
||||||
final selectedValueColor = enabled
|
final selectedValueColor = enabled
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
|
|
@ -40,7 +39,9 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
? const Icon(Symbols.time_auto)
|
? const Icon(Symbols.time_auto)
|
||||||
: const Icon(Symbols.timer_off),
|
: const Icon(Symbols.timer_off),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.sleepTimerSettings(
|
appSettings.copyWith.sleepTimerSettings(
|
||||||
autoTurnOnTimer: value,
|
autoTurnOnTimer: value,
|
||||||
),
|
),
|
||||||
|
|
@ -63,7 +64,9 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
|
initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
|
||||||
);
|
);
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.sleepTimerSettings(
|
appSettings.copyWith.sleepTimerSettings(
|
||||||
autoTurnOnTime: selected.toDuration(),
|
autoTurnOnTime: selected.toDuration(),
|
||||||
),
|
),
|
||||||
|
|
@ -89,7 +92,9 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
|
initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
|
||||||
);
|
);
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.sleepTimerSettings(
|
appSettings.copyWith.sleepTimerSettings(
|
||||||
autoTurnOffTime: selected.toDuration(),
|
autoTurnOffTime: selected.toDuration(),
|
||||||
),
|
),
|
||||||
|
|
@ -97,9 +102,9 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
sleepTimerSettings.autoTurnOffTime
|
sleepTimerSettings.autoTurnOffTime.toTimeOfDay().format(
|
||||||
.toTimeOfDay()
|
context,
|
||||||
.format(context),
|
),
|
||||||
style: TextStyle(color: selectedValueColor),
|
style: TextStyle(color: selectedValueColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -112,7 +117,9 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||||
'Always turn on the sleep timer, no matter what',
|
'Always turn on the sleep timer, no matter what',
|
||||||
),
|
),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.sleepTimerSettings(
|
appSettings.copyWith.sleepTimerSettings(
|
||||||
alwaysAutoTurnOnTimer: value,
|
alwaysAutoTurnOnTimer: value,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,18 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class OkButton<T> extends StatelessWidget {
|
class OkButton<T> extends StatelessWidget {
|
||||||
const OkButton({
|
const OkButton({super.key, this.onPressed});
|
||||||
super.key,
|
|
||||||
this.onPressed,
|
|
||||||
});
|
|
||||||
|
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextButton(
|
return TextButton(onPressed: onPressed, child: const Text('OK'));
|
||||||
onPressed: onPressed,
|
|
||||||
child: const Text('OK'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CancelButton extends StatelessWidget {
|
class CancelButton extends StatelessWidget {
|
||||||
const CancelButton({
|
const CancelButton({super.key, this.onPressed});
|
||||||
super.key,
|
|
||||||
this.onPressed,
|
|
||||||
});
|
|
||||||
|
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ class HomePageSettingsPage extends HookConsumerWidget {
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
initialValue: appSettings
|
initialValue: appSettings
|
||||||
.homePageSettings.showPlayButtonOnContinueListeningShelf,
|
.homePageSettings
|
||||||
|
.showPlayButtonOnContinueListeningShelf,
|
||||||
title: const Text('Continue Listening'),
|
title: const Text('Continue Listening'),
|
||||||
leading: const Icon(Icons.play_arrow),
|
leading: const Icon(Icons.play_arrow),
|
||||||
description: const Text(
|
description: const Text(
|
||||||
|
|
@ -48,7 +49,8 @@ class HomePageSettingsPage extends HookConsumerWidget {
|
||||||
'Show play button for books in continue series shelf',
|
'Show play button for books in continue series shelf',
|
||||||
),
|
),
|
||||||
initialValue: appSettings
|
initialValue: appSettings
|
||||||
.homePageSettings.showPlayButtonOnContinueSeriesShelf,
|
.homePageSettings
|
||||||
|
.showPlayButtonOnContinueSeriesShelf,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
appSettingsNotifier.update(
|
appSettingsNotifier.update(
|
||||||
appSettings.copyWith(
|
appSettings.copyWith(
|
||||||
|
|
@ -66,7 +68,8 @@ class HomePageSettingsPage extends HookConsumerWidget {
|
||||||
'Show play button for all books in all remaining shelves',
|
'Show play button for all books in all remaining shelves',
|
||||||
),
|
),
|
||||||
initialValue: appSettings
|
initialValue: appSettings
|
||||||
.homePageSettings.showPlayButtonOnAllRemainingShelves,
|
.homePageSettings
|
||||||
|
.showPlayButtonOnAllRemainingShelves,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
appSettingsNotifier.update(
|
appSettingsNotifier.update(
|
||||||
appSettings.copyWith(
|
appSettings.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/shared/extensions/enum.dart';
|
import 'package:vaani/shared/extensions/enum.dart';
|
||||||
|
|
||||||
class NotificationSettingsPage extends HookConsumerWidget {
|
class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
const NotificationSettingsPage({
|
const NotificationSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -59,7 +57,9 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (selectedTitle != null) {
|
if (selectedTitle != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
primaryTitle: selectedTitle,
|
primaryTitle: selectedTitle,
|
||||||
),
|
),
|
||||||
|
|
@ -97,7 +97,9 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (selectedTitle != null) {
|
if (selectedTitle != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
secondaryTitle: selectedTitle,
|
secondaryTitle: selectedTitle,
|
||||||
),
|
),
|
||||||
|
|
@ -118,7 +120,9 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
child: TimeIntervalSlider(
|
child: TimeIntervalSlider(
|
||||||
defaultValue: notificationSettings.fastForwardInterval,
|
defaultValue: notificationSettings.fastForwardInterval,
|
||||||
onChangedEnd: (interval) {
|
onChangedEnd: (interval) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
fastForwardInterval: interval,
|
fastForwardInterval: interval,
|
||||||
),
|
),
|
||||||
|
|
@ -141,7 +145,9 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
child: TimeIntervalSlider(
|
child: TimeIntervalSlider(
|
||||||
defaultValue: notificationSettings.rewindInterval,
|
defaultValue: notificationSettings.rewindInterval,
|
||||||
onChangedEnd: (interval) {
|
onChangedEnd: (interval) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
rewindInterval: interval,
|
rewindInterval: interval,
|
||||||
),
|
),
|
||||||
|
|
@ -162,26 +168,23 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
trailing: Wrap(
|
trailing: Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: notificationSettings.mediaControls
|
children: notificationSettings.mediaControls
|
||||||
.map(
|
.map((control) => Icon(control.icon, color: primaryColor))
|
||||||
(control) => Icon(
|
|
||||||
control.icon,
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
final selectedControls =
|
final selectedControls =
|
||||||
await showDialog<List<NotificationMediaControl>>(
|
await showDialog<List<NotificationMediaControl>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return MediaControlsPicker(
|
return MediaControlsPicker(
|
||||||
selectedControls: notificationSettings.mediaControls,
|
selectedControls: notificationSettings.mediaControls,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selectedControls != null) {
|
if (selectedControls != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
mediaControls: selectedControls,
|
mediaControls: selectedControls,
|
||||||
),
|
),
|
||||||
|
|
@ -194,11 +197,14 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: const Text('Show Chapter Progress'),
|
title: const Text('Show Chapter Progress'),
|
||||||
leading: const Icon(Icons.book),
|
leading: const Icon(Icons.book),
|
||||||
description:
|
description: const Text(
|
||||||
const Text('instead of the overall progress of the book'),
|
'instead of the overall progress of the book',
|
||||||
|
),
|
||||||
initialValue: notificationSettings.progressBarIsChapterProgress,
|
initialValue: notificationSettings.progressBarIsChapterProgress,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.notificationSettings(
|
appSettings.copyWith.notificationSettings(
|
||||||
progressBarIsChapterProgress: value,
|
progressBarIsChapterProgress: value,
|
||||||
),
|
),
|
||||||
|
|
@ -213,10 +219,7 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaControlsPicker extends HookConsumerWidget {
|
class MediaControlsPicker extends HookConsumerWidget {
|
||||||
const MediaControlsPicker({
|
const MediaControlsPicker({super.key, required this.selectedControls});
|
||||||
super.key,
|
|
||||||
required this.selectedControls,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<NotificationMediaControl> selectedControls;
|
final List<NotificationMediaControl> selectedControls;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||||
|
|
||||||
class PlayerSettingsPage extends HookConsumerWidget {
|
class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
const PlayerSettingsPage({
|
const PlayerSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -37,7 +35,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
initialValue: playerSettings.configurePlayerForEveryBook,
|
initialValue: playerSettings.configurePlayerForEveryBook,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
configurePlayerForEveryBook: value,
|
configurePlayerForEveryBook: value,
|
||||||
),
|
),
|
||||||
|
|
@ -50,8 +50,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
title: const Text('Default Speed'),
|
title: const Text('Default Speed'),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'${playerSettings.preferredDefaultSpeed}x',
|
'${playerSettings.preferredDefaultSpeed}x',
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
|
color: primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: const Icon(Icons.speed),
|
leading: const Icon(Icons.speed),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
|
|
@ -62,7 +64,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newSpeed != null) {
|
if (newSpeed != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
preferredDefaultSpeed: newSpeed,
|
preferredDefaultSpeed: newSpeed,
|
||||||
),
|
),
|
||||||
|
|
@ -75,8 +79,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
title: const Text('Speed Options'),
|
title: const Text('Speed Options'),
|
||||||
description: Text(
|
description: Text(
|
||||||
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
|
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: const Icon(Icons.speed),
|
leading: const Icon(Icons.speed),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
|
|
@ -87,7 +93,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newSpeedOptions != null) {
|
if (newSpeedOptions != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
speedOptions: newSpeedOptions..sort(),
|
speedOptions: newSpeedOptions..sort(),
|
||||||
),
|
),
|
||||||
|
|
@ -110,7 +118,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.minimumPositionForReporting.smartBinaryFormat,
|
.minimumPositionForReporting
|
||||||
|
.smartBinaryFormat,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryColor,
|
color: primaryColor,
|
||||||
|
|
@ -133,7 +142,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (newDuration != null) {
|
if (newDuration != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
minimumPositionForReporting: newDuration,
|
minimumPositionForReporting: newDuration,
|
||||||
),
|
),
|
||||||
|
|
@ -150,7 +161,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
.markCompleteWhenTimeLeft
|
||||||
|
.smartBinaryFormat,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryColor,
|
color: primaryColor,
|
||||||
|
|
@ -173,7 +185,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (newDuration != null) {
|
if (newDuration != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
markCompleteWhenTimeLeft: newDuration,
|
markCompleteWhenTimeLeft: newDuration,
|
||||||
),
|
),
|
||||||
|
|
@ -190,7 +204,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.playbackReportInterval.smartBinaryFormat,
|
.playbackReportInterval
|
||||||
|
.smartBinaryFormat,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryColor,
|
color: primaryColor,
|
||||||
|
|
@ -213,7 +228,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (newDuration != null) {
|
if (newDuration != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
playbackReportInterval: newDuration,
|
playbackReportInterval: newDuration,
|
||||||
),
|
),
|
||||||
|
|
@ -237,7 +254,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
initialValue:
|
initialValue:
|
||||||
playerSettings.expandedPlayerSettings.showTotalProgress,
|
playerSettings.expandedPlayerSettings.showTotalProgress,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings
|
appSettings.copyWith.playerSettings
|
||||||
.expandedPlayerSettings(showTotalProgress: value),
|
.expandedPlayerSettings(showTotalProgress: value),
|
||||||
);
|
);
|
||||||
|
|
@ -253,7 +272,9 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
initialValue:
|
initialValue:
|
||||||
playerSettings.expandedPlayerSettings.showChapterProgress,
|
playerSettings.expandedPlayerSettings.showChapterProgress,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.playerSettings(
|
appSettings.copyWith.playerSettings(
|
||||||
expandedPlayerSettings: playerSettings
|
expandedPlayerSettings: playerSettings
|
||||||
.expandedPlayerSettings
|
.expandedPlayerSettings
|
||||||
|
|
@ -306,17 +327,15 @@ class TimeDurationSelector extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpeedPicker extends HookConsumerWidget {
|
class SpeedPicker extends HookConsumerWidget {
|
||||||
const SpeedPicker({
|
const SpeedPicker({super.key, this.initialValue = 1});
|
||||||
super.key,
|
|
||||||
this.initialValue = 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double initialValue;
|
final double initialValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final speedController =
|
final speedController = useTextEditingController(
|
||||||
useTextEditingController(text: initialValue.toString());
|
text: initialValue.toString(),
|
||||||
|
);
|
||||||
final speed = useState<double?>(initialValue);
|
final speed = useState<double?>(initialValue);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Select Speed'),
|
title: const Text('Select Speed'),
|
||||||
|
|
@ -368,30 +387,32 @@ class SpeedOptionsPicker extends HookConsumerWidget {
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: speedOptions.value
|
children:
|
||||||
.map(
|
speedOptions.value
|
||||||
(speed) => Chip(
|
.map(
|
||||||
label: Text('${speed}x'),
|
(speed) => Chip(
|
||||||
onDeleted: speed == 1
|
label: Text('${speed}x'),
|
||||||
? null
|
onDeleted: speed == 1
|
||||||
: () {
|
? null
|
||||||
speedOptions.value =
|
: () {
|
||||||
speedOptions.value.where((element) {
|
speedOptions.value = speedOptions.value.where((
|
||||||
// speed option 1 can't be removed
|
element,
|
||||||
return element != speed;
|
) {
|
||||||
}).toList();
|
// speed option 1 can't be removed
|
||||||
},
|
return element != speed;
|
||||||
),
|
}).toList();
|
||||||
)
|
},
|
||||||
.toList()
|
),
|
||||||
..sort((a, b) {
|
)
|
||||||
// if (a.label == const Text('1x')) {
|
.toList()
|
||||||
// return -1;
|
..sort((a, b) {
|
||||||
// } else if (b.label == const Text('1x')) {
|
// if (a.label == const Text('1x')) {
|
||||||
// return 1;
|
// return -1;
|
||||||
// }
|
// } else if (b.label == const Text('1x')) {
|
||||||
return a.label.toString().compareTo(b.label.toString());
|
// return 1;
|
||||||
}),
|
// }
|
||||||
|
return a.label.toString().compareTo(b.label.toString());
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/shared/extensions/enum.dart';
|
import 'package:vaani/shared/extensions/enum.dart';
|
||||||
|
|
||||||
class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
const ShakeDetectorSettingsPage({
|
const ShakeDetectorSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -41,7 +39,9 @@ class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
initialValue: shakeDetectionSettings.isEnabled,
|
initialValue: shakeDetectionSettings.isEnabled,
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.shakeDetectionSettings(
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
isEnabled: value,
|
isEnabled: value,
|
||||||
),
|
),
|
||||||
|
|
@ -77,7 +77,9 @@ class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newThreshold != null) {
|
if (newThreshold != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.shakeDetectionSettings(
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
threshold: newThreshold,
|
threshold: newThreshold,
|
||||||
),
|
),
|
||||||
|
|
@ -107,7 +109,9 @@ class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newShakeAction != null) {
|
if (newShakeAction != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.shakeDetectionSettings(
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
shakeAction: newShakeAction,
|
shakeAction: newShakeAction,
|
||||||
),
|
),
|
||||||
|
|
@ -131,26 +135,23 @@ class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
)
|
)
|
||||||
: Wrap(
|
: Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: shakeDetectionSettings.feedback.map(
|
children: shakeDetectionSettings.feedback.map((feedback) {
|
||||||
(feedback) {
|
return Icon(feedback.icon, color: selectedValueColor);
|
||||||
return Icon(
|
}).toList(),
|
||||||
feedback.icon,
|
|
||||||
color: selectedValueColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
),
|
),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
final newFeedback =
|
final newFeedback =
|
||||||
await showDialog<Set<ShakeDetectedFeedback>>(
|
await showDialog<Set<ShakeDetectedFeedback>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ShakeFeedbackSelector(
|
builder: (context) => ShakeFeedbackSelector(
|
||||||
initialValue: shakeDetectionSettings.feedback,
|
initialValue: shakeDetectionSettings.feedback,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newFeedback != null) {
|
if (newFeedback != null) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.shakeDetectionSettings(
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
feedback: newFeedback,
|
feedback: newFeedback,
|
||||||
),
|
),
|
||||||
|
|
@ -256,10 +257,7 @@ class ShakeActionSelector extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShakeForceSelector extends HookConsumerWidget {
|
class ShakeForceSelector extends HookConsumerWidget {
|
||||||
const ShakeForceSelector({
|
const ShakeForceSelector({super.key, this.initialValue = 6});
|
||||||
super.key,
|
|
||||||
this.initialValue = 6,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double initialValue;
|
final double initialValue;
|
||||||
|
|
||||||
|
|
@ -291,9 +289,7 @@ class ShakeForceSelector extends HookConsumerWidget {
|
||||||
shakeForce.value = 0;
|
shakeForce.value = 0;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
helper: const Text(
|
helper: const Text('Enter a number to set the threshold in m/s²'),
|
||||||
'Enter a number to set the threshold in m/s²',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
|
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
|
||||||
|
|
||||||
class SimpleSettingsPage extends HookConsumerWidget {
|
class SimpleSettingsPage extends HookConsumerWidget {
|
||||||
const SimpleSettingsPage({
|
const SimpleSettingsPage({super.key, this.title, this.sections});
|
||||||
super.key,
|
|
||||||
this.title,
|
|
||||||
this.sections,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget? title;
|
final Widget? title;
|
||||||
final List<AbstractSettingsSection>? sections;
|
final List<AbstractSettingsSection>? sections;
|
||||||
|
|
@ -34,18 +30,16 @@ class SimpleSettingsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
if (sections != null)
|
if (sections != null)
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate([
|
||||||
[
|
ClipRRect(
|
||||||
ClipRRect(
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
child: SettingsList(
|
||||||
child: SettingsList(
|
shrinkWrap: true,
|
||||||
shrinkWrap: true,
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
sections: sections!,
|
||||||
sections: sections!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
]),
|
||||||
),
|
),
|
||||||
// some padding at the bottom
|
// some padding at the bottom
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
|
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
import 'package:vaani/shared/extensions/enum.dart';
|
import 'package:vaani/shared/extensions/enum.dart';
|
||||||
|
|
||||||
class ThemeSettingsPage extends HookConsumerWidget {
|
class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
const ThemeSettingsPage({
|
const ThemeSettingsPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -38,7 +36,9 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
selectedIcon: const Icon(Icons.check),
|
selectedIcon: const Icon(Icons.check),
|
||||||
selected: {themeSettings.themeMode},
|
selected: {themeSettings.themeMode},
|
||||||
onSelectionChanged: (newSelection) {
|
onSelectionChanged: (newSelection) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.themeSettings(
|
appSettings.copyWith.themeSettings(
|
||||||
themeMode: newSelection.first,
|
themeMode: newSelection.first,
|
||||||
),
|
),
|
||||||
|
|
@ -66,8 +66,8 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
themeSettings.themeMode == ThemeMode.light
|
themeSettings.themeMode == ThemeMode.light
|
||||||
? Icons.light_mode
|
? Icons.light_mode
|
||||||
: themeSettings.themeMode == ThemeMode.dark
|
: themeSettings.themeMode == ThemeMode.dark
|
||||||
? Icons.dark_mode
|
? Icons.dark_mode
|
||||||
: Icons.auto_awesome,
|
: Icons.auto_awesome,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -82,10 +82,10 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
'Increase the contrast between the background and the text',
|
'Increase the contrast between the background and the text',
|
||||||
),
|
),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
appSettings.copyWith.themeSettings(
|
.read(appSettingsProvider.notifier)
|
||||||
highContrast: value,
|
.update(
|
||||||
),
|
appSettings.copyWith.themeSettings(highContrast: value),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -103,7 +103,9 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
? const Icon(Icons.auto_awesome)
|
? const Icon(Icons.auto_awesome)
|
||||||
: const Icon(Icons.auto_fix_off),
|
: const Icon(Icons.auto_fix_off),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.themeSettings(
|
appSettings.copyWith.themeSettings(
|
||||||
useMaterialThemeFromSystem: value,
|
useMaterialThemeFromSystem: value,
|
||||||
),
|
),
|
||||||
|
|
@ -164,7 +166,9 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
? const Icon(Icons.auto_fix_high)
|
? const Icon(Icons.auto_fix_high)
|
||||||
: const Icon(Icons.auto_fix_off),
|
: const Icon(Icons.auto_fix_off),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.themeSettings(
|
appSettings.copyWith.themeSettings(
|
||||||
useCurrentPlayerThemeThroughoutApp: value,
|
useCurrentPlayerThemeThroughoutApp: value,
|
||||||
),
|
),
|
||||||
|
|
@ -182,7 +186,9 @@ class ThemeSettingsPage extends HookConsumerWidget {
|
||||||
? const Icon(Icons.auto_fix_high)
|
? const Icon(Icons.auto_fix_high)
|
||||||
: const Icon(Icons.auto_fix_off),
|
: const Icon(Icons.auto_fix_off),
|
||||||
onToggle: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.update(
|
||||||
appSettings.copyWith.themeSettings(
|
appSettings.copyWith.themeSettings(
|
||||||
useMaterialThemeOnItemPage: value,
|
useMaterialThemeOnItemPage: value,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,7 @@ class NavigationWithSwitchTile extends AbstractSettingsTile {
|
||||||
indent: 8.0,
|
indent: 8.0,
|
||||||
endIndent: 8.0,
|
endIndent: 8.0,
|
||||||
),
|
),
|
||||||
Switch.adaptive(
|
Switch.adaptive(value: value, onChanged: onToggle),
|
||||||
value: value,
|
|
||||||
onChanged: onToggle,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,7 @@ extension TitleCase on Enum {
|
||||||
String get pascalCase {
|
String get pascalCase {
|
||||||
// capitalize the first letter of each word
|
// capitalize the first letter of each word
|
||||||
return name
|
return name
|
||||||
.replaceAllMapped(
|
.replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}')
|
||||||
RegExp(r'([A-Z])'),
|
|
||||||
(match) => ' ${match.group(0)}',
|
|
||||||
)
|
|
||||||
.trim()
|
.trim()
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map((word) => word[0].toUpperCase() + word.substring(1))
|
.map((word) => word[0].toUpperCase() + word.substring(1))
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ extension ShelfConversion on Shelf {
|
||||||
|
|
||||||
extension UserConversion on User {
|
extension UserConversion on User {
|
||||||
UserWithSessionAndMostRecentProgress
|
UserWithSessionAndMostRecentProgress
|
||||||
get asUserWithSessionAndMostRecentProgress =>
|
get asUserWithSessionAndMostRecentProgress =>
|
||||||
UserWithSessionAndMostRecentProgress.fromJson(toJson());
|
UserWithSessionAndMostRecentProgress.fromJson(toJson());
|
||||||
User get asUser => User.fromJson(toJson());
|
User get asUser => User.fromJson(toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,7 @@ extension ObfuscateServer on AudiobookShelfServer {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return copyWith(
|
return copyWith(serverUrl: serverUrl.obfuscate());
|
||||||
serverUrl: serverUrl.obfuscate(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,10 +101,7 @@ extension ObfuscateRequest on http.BaseRequest {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return http.Request(
|
return http.Request(method, url.obfuscate());
|
||||||
method,
|
|
||||||
url.obfuscate(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,9 +129,11 @@ extension ObfuscateResponse on http.Response {
|
||||||
// token regex is `"token": "..."`
|
// token regex is `"token": "..."`
|
||||||
return body
|
return body
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
RegExp(r'(\b\w+@\w+\.\w+\b)|'
|
RegExp(
|
||||||
r'(\b\d{3}-\d{3}-\d{4}\b)|'
|
r'(\b\w+@\w+\.\w+\b)|'
|
||||||
r'(\bhttps?://\S+\b)'),
|
r'(\b\d{3}-\d{3}-\d{4}\b)|'
|
||||||
|
r'(\bhttps?://\S+\b)',
|
||||||
|
),
|
||||||
'obfuscated',
|
'obfuscated',
|
||||||
)
|
)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
|
|
@ -151,9 +148,7 @@ extension ObfuscateLoginResponse on shelfsdk.LoginResponse {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return copyWith(
|
return copyWith(user: user.obfuscate());
|
||||||
user: user.obfuscate(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,8 +157,6 @@ extension ObfuscateUser on shelfsdk.User {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return shelfsdk.User.fromJson(
|
return shelfsdk.User.fromJson(toJson()..['token'] = 'tokenObfuscated');
|
||||||
toJson()..['token'] = 'tokenObfuscated',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension ToTimeOfDay on Duration {
|
extension ToTimeOfDay on Duration {
|
||||||
TimeOfDay toTimeOfDay() {
|
TimeOfDay toTimeOfDay() {
|
||||||
return TimeOfDay(
|
return TimeOfDay(hour: inHours % 24, minute: inMinutes % 60);
|
||||||
hour: inHours % 24,
|
|
||||||
minute: inMinutes % 60,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,18 @@ void useInterval(VoidCallback callback, Duration delay) {
|
||||||
final savedCallback = useRef(callback);
|
final savedCallback = useRef(callback);
|
||||||
savedCallback.value = callback;
|
savedCallback.value = callback;
|
||||||
|
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
final timer = Timer.periodic(delay, (_) => savedCallback.value());
|
||||||
final timer = Timer.periodic(delay, (_) => savedCallback.value());
|
return timer.cancel;
|
||||||
return timer.cancel;
|
}, [delay]);
|
||||||
},
|
|
||||||
[delay],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void useTimer(VoidCallback callback, Duration delay) {
|
void useTimer(VoidCallback callback, Duration delay) {
|
||||||
final savedCallback = useRef(callback);
|
final savedCallback = useRef(callback);
|
||||||
savedCallback.value = callback;
|
savedCallback.value = callback;
|
||||||
|
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
final timer = Timer(delay, savedCallback.value);
|
||||||
final timer = Timer(delay, savedCallback.value);
|
return timer.cancel;
|
||||||
return timer.cancel;
|
}, [delay]);
|
||||||
},
|
|
||||||
[delay],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,46 +24,106 @@ class AbsIcons {
|
||||||
static const _kFontFam = 'AbsIcons';
|
static const _kFontFam = 'AbsIcons';
|
||||||
static const String? _kFontPkg = null;
|
static const String? _kFontPkg = null;
|
||||||
|
|
||||||
static const IconData audiobookshelf =
|
static const IconData audiobookshelf = IconData(
|
||||||
IconData(0xe900, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
0xe900,
|
||||||
static const IconData microphone_2 =
|
fontFamily: _kFontFam,
|
||||||
IconData(0xe901, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontPackage: _kFontPkg,
|
||||||
static const IconData microphone_1 =
|
);
|
||||||
IconData(0xe902, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
static const IconData microphone_2 = IconData(
|
||||||
static const IconData radio =
|
0xe901,
|
||||||
IconData(0xe903, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontFamily: _kFontFam,
|
||||||
static const IconData podcast =
|
fontPackage: _kFontPkg,
|
||||||
IconData(0xe904, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
);
|
||||||
static const IconData books_1 =
|
static const IconData microphone_1 = IconData(
|
||||||
IconData(0xe905, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
0xe902,
|
||||||
static const IconData database_2 =
|
fontFamily: _kFontFam,
|
||||||
IconData(0xe906, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontPackage: _kFontPkg,
|
||||||
static const IconData headphones =
|
);
|
||||||
IconData(0xe910, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
static const IconData radio = IconData(
|
||||||
static const IconData music =
|
0xe903,
|
||||||
IconData(0xe911, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontFamily: _kFontFam,
|
||||||
static const IconData video =
|
fontPackage: _kFontPkg,
|
||||||
IconData(0xe914, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
);
|
||||||
static const IconData microphone_3 =
|
static const IconData podcast = IconData(
|
||||||
IconData(0xe91e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
0xe904,
|
||||||
static const IconData book =
|
fontFamily: _kFontFam,
|
||||||
IconData(0xe91f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontPackage: _kFontPkg,
|
||||||
static const IconData books_2 =
|
);
|
||||||
IconData(0xe920, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
static const IconData books_1 = IconData(
|
||||||
static const IconData file_picture =
|
0xe905,
|
||||||
IconData(0xe927, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontFamily: _kFontFam,
|
||||||
static const IconData database_1 =
|
fontPackage: _kFontPkg,
|
||||||
IconData(0xe964, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
);
|
||||||
static const IconData rocket =
|
static const IconData database_2 = IconData(
|
||||||
IconData(0xe9a5, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
0xe906,
|
||||||
static const IconData power =
|
fontFamily: _kFontFam,
|
||||||
IconData(0xe9b5, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontPackage: _kFontPkg,
|
||||||
static const IconData star =
|
);
|
||||||
IconData(0xe9d9, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
static const IconData headphones = IconData(
|
||||||
static const IconData heart =
|
0xe910,
|
||||||
IconData(0xe9da, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
fontFamily: _kFontFam,
|
||||||
static const IconData rss =
|
fontPackage: _kFontPkg,
|
||||||
IconData(0xea9b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
);
|
||||||
|
static const IconData music = IconData(
|
||||||
|
0xe911,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData video = IconData(
|
||||||
|
0xe914,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData microphone_3 = IconData(
|
||||||
|
0xe91e,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData book = IconData(
|
||||||
|
0xe91f,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData books_2 = IconData(
|
||||||
|
0xe920,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData file_picture = IconData(
|
||||||
|
0xe927,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData database_1 = IconData(
|
||||||
|
0xe964,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData rocket = IconData(
|
||||||
|
0xe9a5,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData power = IconData(
|
||||||
|
0xe9b5,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData star = IconData(
|
||||||
|
0xe9d9,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData heart = IconData(
|
||||||
|
0xe9da,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
static const IconData rss = IconData(
|
||||||
|
0xea9b,
|
||||||
|
fontFamily: _kFontFam,
|
||||||
|
fontPackage: _kFontPkg,
|
||||||
|
);
|
||||||
|
|
||||||
static final Map<String, IconData> _iconMap = {
|
static final Map<String, IconData> _iconMap = {
|
||||||
'audiobookshelf': audiobookshelf,
|
'audiobookshelf': audiobookshelf,
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ class AddNewServer extends HookConsumerWidget {
|
||||||
// do nothing
|
// do nothing
|
||||||
appLogger.severe('Error parsing URI: $e');
|
appLogger.severe('Error parsing URI: $e');
|
||||||
}
|
}
|
||||||
final canSubmit = !readOnly &&
|
final canSubmit =
|
||||||
|
!readOnly &&
|
||||||
(isServerAliveValue || (allowEmpty && newServerURI.text.isEmpty));
|
(isServerAliveValue || (allowEmpty && newServerURI.text.isEmpty));
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
|
|
@ -71,8 +72,9 @@ class AddNewServer extends HookConsumerWidget {
|
||||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.8),
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixText:
|
prefixText: myController.text.startsWith(httpUrlRegExp)
|
||||||
myController.text.startsWith(httpUrlRegExp) ? '' : 'https://',
|
? ''
|
||||||
|
: 'https://',
|
||||||
prefixIcon: ServerAliveIcon(server: parsedUri),
|
prefixIcon: ServerAliveIcon(server: parsedUri),
|
||||||
|
|
||||||
// add server button
|
// add server button
|
||||||
|
|
@ -101,10 +103,7 @@ class AddNewServer extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerAliveIcon extends HookConsumerWidget {
|
class ServerAliveIcon extends HookConsumerWidget {
|
||||||
const ServerAliveIcon({
|
const ServerAliveIcon({super.key, required this.server});
|
||||||
super.key,
|
|
||||||
required this.server,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
|
|
||||||
|
|
@ -121,8 +120,8 @@ class ServerAliveIcon extends HookConsumerWidget {
|
||||||
message: server.toString().isEmpty
|
message: server.toString().isEmpty
|
||||||
? 'Server Status'
|
? 'Server Status'
|
||||||
: isServerAliveValue
|
: isServerAliveValue
|
||||||
? 'Server connected'
|
? 'Server connected'
|
||||||
: 'Cannot connect to server',
|
: 'Cannot connect to server',
|
||||||
child: server.toString().isEmpty
|
child: server.toString().isEmpty
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.cloud_outlined,
|
Icons.cloud_outlined,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import 'package:vaani/features/you/view/server_manager.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
|
|
||||||
class MyDrawer extends StatelessWidget {
|
class MyDrawer extends StatelessWidget {
|
||||||
const MyDrawer({
|
const MyDrawer({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -16,10 +14,7 @@ class MyDrawer extends StatelessWidget {
|
||||||
const DrawerHeader(
|
const DrawerHeader(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Vaani',
|
'Vaani',
|
||||||
style: TextStyle(
|
style: TextStyle(fontStyle: FontStyle.italic, fontSize: 30),
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 30,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,7 @@ class ExpandableDescription extends HookWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
// header text
|
// header text
|
||||||
Text(
|
Text(style: textTheme.titleMedium, title),
|
||||||
style: textTheme.titleMedium,
|
|
||||||
title,
|
|
||||||
),
|
|
||||||
// carrot icon
|
// carrot icon
|
||||||
AnimatedRotation(
|
AnimatedRotation(
|
||||||
turns: isDescExpanded.value ? 0.5 : 0,
|
turns: isDescExpanded.value ? 0.5 : 0,
|
||||||
|
|
@ -79,11 +76,7 @@ class ExpandableDescription extends HookWidget {
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: duration * 3,
|
duration: duration * 3,
|
||||||
child: isDescExpanded.value
|
child: isDescExpanded.value
|
||||||
? Text(
|
? Text(style: textTheme.bodyMedium, content, maxLines: null)
|
||||||
style: textTheme.bodyMedium,
|
|
||||||
content,
|
|
||||||
maxLines: null,
|
|
||||||
)
|
|
||||||
: Text(
|
: Text(
|
||||||
style: textTheme.bodyMedium,
|
style: textTheme.bodyMedium,
|
||||||
content,
|
content,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void showNotImplementedToast(BuildContext context) {
|
void showNotImplementedToast(BuildContext context) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text("Not implemented"), showCloseIcon: true),
|
||||||
content: Text("Not implemented"),
|
|
||||||
showCloseIcon: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,7 @@ import 'package:vaani/shared/widgets/shelves/home_shelf.dart';
|
||||||
|
|
||||||
/// A shelf that displays Authors on the home page
|
/// A shelf that displays Authors on the home page
|
||||||
class AuthorHomeShelf extends HookConsumerWidget {
|
class AuthorHomeShelf extends HookConsumerWidget {
|
||||||
const AuthorHomeShelf({
|
const AuthorHomeShelf({super.key, required this.shelf, required this.title});
|
||||||
super.key,
|
|
||||||
required this.shelf,
|
|
||||||
required this.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final AuthorShelf shelf;
|
final AuthorShelf shelf;
|
||||||
|
|
@ -20,9 +16,7 @@ class AuthorHomeShelf extends HookConsumerWidget {
|
||||||
return SimpleHomeShelf(
|
return SimpleHomeShelf(
|
||||||
title: title,
|
title: title,
|
||||||
children: shelf.entities
|
children: shelf.entities
|
||||||
.map(
|
.map((item) => AuthorOnShelf(item: item))
|
||||||
(item) => AuthorOnShelf(item: item),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -30,10 +24,7 @@ class AuthorHomeShelf extends HookConsumerWidget {
|
||||||
|
|
||||||
// a widget to display a item on the shelf
|
// a widget to display a item on the shelf
|
||||||
class AuthorOnShelf extends HookConsumerWidget {
|
class AuthorOnShelf extends HookConsumerWidget {
|
||||||
const AuthorOnShelf({
|
const AuthorOnShelf({super.key, required this.item});
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Author item;
|
final Author item;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ class BookHomeShelf extends HookConsumerWidget {
|
||||||
.map(
|
.map(
|
||||||
(item) => switch (item.mediaType) {
|
(item) => switch (item.mediaType) {
|
||||||
MediaType.book => BookOnShelf(
|
MediaType.book => BookOnShelf(
|
||||||
item: item,
|
item: item,
|
||||||
key: ValueKey(shelf.id + item.id),
|
key: ValueKey(shelf.id + item.id),
|
||||||
heroTagSuffix: shelf.id,
|
heroTagSuffix: shelf.id,
|
||||||
showPlayButton: showPlayButton,
|
showPlayButton: showPlayButton,
|
||||||
),
|
),
|
||||||
_ => Container(),
|
_ => Container(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -83,13 +83,8 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
// open the book
|
// open the book
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
Routes.libraryItem.name,
|
Routes.libraryItem.name,
|
||||||
pathParameters: {
|
pathParameters: {Routes.libraryItem.pathParamName!: item.id},
|
||||||
Routes.libraryItem.pathParamName!: item.id,
|
extra: LibraryItemExtras(book: book, heroTagSuffix: heroTagSuffix),
|
||||||
},
|
|
||||||
extra: LibraryItemExtras(
|
|
||||||
book: book,
|
|
||||||
heroTagSuffix: heroTagSuffix,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,8 +94,11 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
onTap: handleTapOnBook,
|
onTap: handleTapOnBook,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding: const EdgeInsets.only(
|
||||||
const EdgeInsets.only(bottom: 8.0, right: 4.0, left: 4.0),
|
bottom: 8.0,
|
||||||
|
right: 4.0,
|
||||||
|
left: 4.0,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -112,7 +110,8 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: HeroTagPrefixes.bookCover +
|
tag:
|
||||||
|
HeroTagPrefixes.bookCover +
|
||||||
item.id +
|
item.id +
|
||||||
heroTagSuffix,
|
heroTagSuffix,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
@ -128,17 +127,19 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
var imageWidget = Image.memory(
|
var imageWidget = Image.memory(
|
||||||
image,
|
image,
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
cacheWidth: (height *
|
cacheWidth:
|
||||||
1.2 *
|
(height *
|
||||||
MediaQuery.of(context)
|
1.2 *
|
||||||
.devicePixelRatio)
|
MediaQuery.of(
|
||||||
.round(),
|
context,
|
||||||
|
).devicePixelRatio)
|
||||||
|
.round(),
|
||||||
);
|
);
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onPrimaryContainer,
|
).colorScheme.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
child: imageWidget,
|
child: imageWidget,
|
||||||
);
|
);
|
||||||
|
|
@ -157,9 +158,7 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
// a play button on the book cover
|
// a play button on the book cover
|
||||||
if (showPlayButton)
|
if (showPlayButton)
|
||||||
_BookOnShelfPlayButton(
|
_BookOnShelfPlayButton(libraryItemId: item.id),
|
||||||
libraryItemId: item.id,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -202,9 +201,7 @@ class BookOnShelf extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BookOnShelfPlayButton extends HookConsumerWidget {
|
class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
const _BookOnShelfPlayButton({
|
const _BookOnShelfPlayButton({required this.libraryItemId});
|
||||||
required this.libraryItemId,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// the id of the library item of the book
|
/// the id of the library item of the book
|
||||||
final String libraryItemId;
|
final String libraryItemId;
|
||||||
|
|
@ -217,8 +214,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
player.book?.libraryItemId == libraryItemId;
|
player.book?.libraryItemId == libraryItemId;
|
||||||
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer;
|
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer;
|
||||||
|
|
||||||
final userProgress = me.value?.mediaProgress
|
final userProgress = me.value?.mediaProgress?.firstWhereOrNull(
|
||||||
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
(element) => element.libraryItemId == libraryItemId,
|
||||||
|
);
|
||||||
final isBookCompleted = userProgress?.isFinished ?? false;
|
final isBookCompleted = userProgress?.isFinished ?? false;
|
||||||
|
|
||||||
const size = 40.0;
|
const size = 40.0;
|
||||||
|
|
@ -226,8 +224,10 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
// if there is user progress for this book show a circular progress indicator around the play button
|
// if there is user progress for this book show a circular progress indicator around the play button
|
||||||
var strokeWidth = size / 8;
|
var strokeWidth = size / 8;
|
||||||
|
|
||||||
final useMaterialThemeOnItemPage =
|
final useMaterialThemeOnItemPage = ref
|
||||||
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
|
.watch(appSettingsProvider)
|
||||||
|
.themeSettings
|
||||||
|
.useMaterialThemeOnItemPage;
|
||||||
|
|
||||||
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
|
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
|
||||||
if (useMaterialThemeOnItemPage && isCurrentBookSetInPlayer) {
|
if (useMaterialThemeOnItemPage && isCurrentBookSetInPlayer) {
|
||||||
|
|
@ -242,8 +242,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
return Theme(
|
return Theme(
|
||||||
// if current book is set in player, get theme from the cover image
|
// if current book is set in player, get theme from the cover image
|
||||||
data: ThemeData(
|
data: ThemeData(
|
||||||
colorScheme:
|
colorScheme: coverColorScheme.value ?? Theme.of(context).colorScheme,
|
||||||
coverColorScheme.value ?? Theme.of(context).colorScheme,
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(strokeWidth / 2 + 2),
|
padding: EdgeInsets.all(strokeWidth / 2 + 2),
|
||||||
|
|
@ -258,10 +257,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: userProgress.progress,
|
value: userProgress.progress,
|
||||||
strokeWidth: strokeWidth,
|
strokeWidth: strokeWidth,
|
||||||
backgroundColor: Theme.of(context)
|
backgroundColor: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onPrimary
|
).colorScheme.onPrimary.withValues(alpha: 0.8),
|
||||||
.withValues(alpha: 0.8),
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).colorScheme.primary,
|
Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
|
@ -272,22 +270,18 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||||
EdgeInsets.zero,
|
minimumSize: WidgetStateProperty.all(const Size(size, size)),
|
||||||
),
|
|
||||||
minimumSize: WidgetStateProperty.all(
|
|
||||||
const Size(size, size),
|
|
||||||
),
|
|
||||||
backgroundColor: WidgetStateProperty.all(
|
backgroundColor: WidgetStateProperty.all(
|
||||||
Theme.of(context)
|
Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onPrimary
|
).colorScheme.onPrimary.withValues(alpha: 0.9),
|
||||||
.withValues(alpha: 0.9),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final book =
|
final book = await ref.watch(
|
||||||
await ref.watch(libraryItemProvider(libraryItemId).future);
|
libraryItemProvider(libraryItemId).future,
|
||||||
|
);
|
||||||
|
|
||||||
libraryItemPlayButtonOnPressed(
|
libraryItemPlayButtonOnPressed(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
|
|
@ -313,9 +307,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
|
|
||||||
// a skeleton for the book cover
|
// a skeleton for the book cover
|
||||||
class BookCoverSkeleton extends StatelessWidget {
|
class BookCoverSkeleton extends StatelessWidget {
|
||||||
const BookCoverSkeleton({
|
const BookCoverSkeleton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -324,13 +316,13 @@ class BookCoverSkeleton extends StatelessWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 150,
|
width: 150,
|
||||||
child: Shimmer.fromColors(
|
child: Shimmer.fromColors(
|
||||||
baseColor:
|
baseColor: Theme.of(
|
||||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.3),
|
context,
|
||||||
highlightColor:
|
).colorScheme.surface.withValues(alpha: 0.3),
|
||||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.1),
|
highlightColor: Theme.of(
|
||||||
child: Container(
|
context,
|
||||||
color: Theme.of(context).colorScheme.surface,
|
).colorScheme.onSurface.withValues(alpha: 0.1),
|
||||||
),
|
child: Container(color: Theme.of(context).colorScheme.surface),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ class HomeShelf extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return switch (shelf.type) {
|
return switch (shelf.type) {
|
||||||
ShelfType.book => BookHomeShelf(
|
ShelfType.book => BookHomeShelf(
|
||||||
title: title,
|
title: title,
|
||||||
shelf: shelf.asLibraryItemShelf,
|
shelf: shelf.asLibraryItemShelf,
|
||||||
showPlayButton: showPlayButton,
|
showPlayButton: showPlayButton,
|
||||||
),
|
),
|
||||||
ShelfType.authors => AuthorHomeShelf(
|
ShelfType.authors => AuthorHomeShelf(
|
||||||
title: title,
|
title: title,
|
||||||
shelf: shelf.asAuthorShelf,
|
shelf: shelf.asAuthorShelf,
|
||||||
),
|
),
|
||||||
_ => Container(),
|
_ => Container(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -75,9 +75,7 @@ class SimpleHomeShelf extends HookConsumerWidget {
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0 || index == children.length + 1) {
|
if (index == 0 || index == children.length + 1) {
|
||||||
return const SizedBox(
|
return const SizedBox(width: 8);
|
||||||
width: 8,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return children[index - 1];
|
return children[index - 1];
|
||||||
},
|
},
|
||||||
|
|
@ -88,7 +86,8 @@ class SimpleHomeShelf extends HookConsumerWidget {
|
||||||
|
|
||||||
return const SizedBox(width: 4);
|
return const SizedBox(width: 4);
|
||||||
},
|
},
|
||||||
itemCount: children.length +
|
itemCount:
|
||||||
|
children.length +
|
||||||
2, // add some extra space at the start and end so that the first and last items are not at the edge
|
2, // add some extra space at the start and end so that the first and last items are not at the edge
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -53,22 +53,15 @@ FutureOr<(ColorScheme light, ColorScheme dark)?> systemTheme(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemeLight == null || schemeDark == null) {
|
if (schemeLight == null || schemeDark == null) {
|
||||||
_logger
|
_logger.warning(
|
||||||
.warning('dynamic_color: Dynamic color not detected on this device.');
|
'dynamic_color: Dynamic color not detected on this device.',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// set high contrast theme
|
// set high contrast theme
|
||||||
if (highContrast) {
|
if (highContrast) {
|
||||||
schemeLight = schemeLight
|
schemeLight = schemeLight.copyWith(surface: Colors.white).harmonized();
|
||||||
.copyWith(
|
schemeDark = schemeDark.copyWith(surface: Colors.black).harmonized();
|
||||||
surface: Colors.white,
|
|
||||||
)
|
|
||||||
.harmonized();
|
|
||||||
schemeDark = schemeDark
|
|
||||||
.copyWith(
|
|
||||||
surface: Colors.black,
|
|
||||||
)
|
|
||||||
.harmonized();
|
|
||||||
}
|
}
|
||||||
return (schemeLight, schemeDark);
|
return (schemeLight, schemeDark);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue