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