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

This commit is contained in:
Dr.Blank 2026-01-10 16:51:05 +05:30
parent a520136e01
commit e23c0b6c5f
No known key found for this signature in database
GPG key ID: BA5F87FF0560C57B
84 changed files with 1565 additions and 1945 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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