一堆乱七八糟的修改

播放页面增加桌面版
This commit is contained in:
rang 2025-11-28 17:05:35 +08:00
parent aee1fbde88
commit 3ba35b31b8
116 changed files with 1238 additions and 2592 deletions

View file

@ -10,6 +10,7 @@
"deeplinking", "deeplinking",
"fullscreen", "fullscreen",
"Lerp", "Lerp",
"Librarys",
"miniplayer", "miniplayer",
"mocktail", "mocktail",
"nodename", "nodename",

View file

@ -9,10 +9,11 @@ import 'package:http/http.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/db/cache/cache_key.dart';
import 'package:vaani/db/cache_manager.dart'; import 'package:vaani/db/cache_manager.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/authenticated_user.dart'; import 'package:vaani/features/settings/models/authenticated_user.dart';
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_provider.g.dart'; part 'api_provider.g.dart';
@ -154,15 +155,14 @@ class PersonalizedView extends _$PersonalizedView {
} }
// try to find in cache // try to find in cache
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}'; // final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}'; // final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
final cachedRes = await apiResponseCacheManager.getFileFromMemory( final key = CacheKey.personalized(apiSettings.activeLibraryId! + user.id);
key, final cachedFile = await apiResponseCacheManager.getFileFromCache(key);
) ?? if (cachedFile != null) {
await apiResponseCacheManager.getFileFromCache(key); _logger.fine('reading from cache: $cachedFile for key: $key');
if (cachedRes != null) {
_logger.fine('reading from cache: $cachedRes for key: $key');
try { try {
final resJson = jsonDecode(await cachedRes.file.readAsString()) as List; final resJson =
jsonDecode(await cachedFile.file.readAsString()) as List;
final res = [ final res = [
for (final item in resJson) for (final item in resJson)
Shelf.fromJson(item as Map<String, dynamic>), Shelf.fromJson(item as Map<String, dynamic>),
@ -170,7 +170,7 @@ class PersonalizedView extends _$PersonalizedView {
_logger.fine('successfully read from cache key: $key'); _logger.fine('successfully read from cache key: $key');
yield res; yield res;
} catch (e) { } catch (e) {
_logger.warning('error reading from cache: $e\n$cachedRes'); _logger.warning('error reading from cache: $e\n$cachedFile');
} }
} }

View file

@ -662,7 +662,7 @@ class _LoginProviderElement
AuthenticatedUser? get user => (origin as LoginProvider).user; AuthenticatedUser? get user => (origin as LoginProvider).user;
} }
String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f'; String _$personalizedViewHash() => r'e3c3e041f925f041db2145e8ca0dbb07268ecc47';
/// fetch the personalized view /// fetch the personalized view
/// ///

View file

@ -3,9 +3,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/server_provider.dart' import 'package:vaani/api/server_provider.dart'
show audiobookShelfServerProvider; show audiobookShelfServerProvider;
import 'package:vaani/db/storage.dart'; import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/features/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart' as model; import 'package:vaani/features/settings/models/authenticated_user.dart'
as model;
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'authenticated_users_provider.g.dart'; part 'authenticated_users_provider.g.dart';

View file

@ -1,11 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/db/cache/cache_key.dart'; import 'package:vaani/db/cache/cache_key.dart';
import 'package:vaani/db/cache_manager.dart'; import 'package:vaani/db/cache_manager.dart';
import 'package:vaani/globals.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
part 'library_item_provider.g.dart'; part 'library_item_provider.g.dart';
@ -26,8 +28,8 @@ class LibraryItem extends _$LibraryItem {
// look for the item in the cache // look for the item in the cache
final key = CacheKey.libraryItem(id); final key = CacheKey.libraryItem(id);
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ?? final cachedFile = await apiResponseCacheManager.getFileFromCache(key);
await apiResponseCacheManager.getFileFromCache(key);
if (cachedFile != null) { if (cachedFile != null) {
_logger.fine( _logger.fine(
'LibraryItemProvider reading from cache for $id from ${cachedFile.file}', 'LibraryItemProvider reading from cache for $id from ${cachedFile.file}',
@ -69,3 +71,39 @@ class LibraryItem extends _$LibraryItem {
} }
} }
} }
@riverpod
Future<shelfsdk.PlaybackSessionExpanded?> playBackSession(
Ref ref,
String libraryItemId,
) async {
final api = ref.watch(authenticatedApiProvider);
final playBack = await api.items.play(
libraryItemId: libraryItemId,
parameters: shelfsdk.PlayItemReqParams(
deviceInfo: shelfsdk.DeviceInfoReqParams(
clientVersion: appVersion,
manufacturer: deviceManufacturer,
model: deviceModel,
sdkVersion: deviceSdkVersion,
clientName: appName,
deviceName: deviceName,
),
forceDirectPlay: false,
forceTranscode: false,
supportedMimeTypes: [
"audio/flac",
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/aac",
"audio/webm",
],
),
);
if (playBack == null) {
return null;
}
return playBack.asExpanded;
}

View file

@ -6,7 +6,7 @@ part of 'library_item_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; String _$playBackSessionHash() => r'1bc00653e0041e839d8569192b6bc90d96b4ca4f';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -29,6 +29,144 @@ class _SystemHash {
} }
} }
/// See also [playBackSession].
@ProviderFor(playBackSession)
const playBackSessionProvider = PlayBackSessionFamily();
/// See also [playBackSession].
class PlayBackSessionFamily
extends Family<AsyncValue<shelfsdk.PlaybackSessionExpanded?>> {
/// See also [playBackSession].
const PlayBackSessionFamily();
/// See also [playBackSession].
PlayBackSessionProvider call(
String libraryItemId,
) {
return PlayBackSessionProvider(
libraryItemId,
);
}
@override
PlayBackSessionProvider getProviderOverride(
covariant PlayBackSessionProvider provider,
) {
return call(
provider.libraryItemId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'playBackSessionProvider';
}
/// See also [playBackSession].
class PlayBackSessionProvider
extends AutoDisposeFutureProvider<shelfsdk.PlaybackSessionExpanded?> {
/// See also [playBackSession].
PlayBackSessionProvider(
String libraryItemId,
) : this._internal(
(ref) => playBackSession(
ref as PlayBackSessionRef,
libraryItemId,
),
from: playBackSessionProvider,
name: r'playBackSessionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$playBackSessionHash,
dependencies: PlayBackSessionFamily._dependencies,
allTransitiveDependencies:
PlayBackSessionFamily._allTransitiveDependencies,
libraryItemId: libraryItemId,
);
PlayBackSessionProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.libraryItemId,
}) : super.internal();
final String libraryItemId;
@override
Override overrideWith(
FutureOr<shelfsdk.PlaybackSessionExpanded?> Function(
PlayBackSessionRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: PlayBackSessionProvider._internal(
(ref) => create(ref as PlayBackSessionRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
libraryItemId: libraryItemId,
),
);
}
@override
AutoDisposeFutureProviderElement<shelfsdk.PlaybackSessionExpanded?>
createElement() {
return _PlayBackSessionProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PlayBackSessionProvider &&
other.libraryItemId == libraryItemId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, libraryItemId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PlayBackSessionRef
on AutoDisposeFutureProviderRef<shelfsdk.PlaybackSessionExpanded?> {
/// The parameter `libraryItemId` of this provider.
String get libraryItemId;
}
class _PlayBackSessionProviderElement
extends AutoDisposeFutureProviderElement<shelfsdk.PlaybackSessionExpanded?>
with PlayBackSessionRef {
_PlayBackSessionProviderElement(super.provider);
@override
String get libraryItemId => (origin as PlayBackSessionProvider).libraryItemId;
}
String _$libraryItemHash() => r'8032b2d3ca6a8a2a6217cd32f11cd4b35919f49e';
abstract class _$LibraryItem abstract class _$LibraryItem
extends BuildlessStreamNotifier<shelfsdk.LibraryItemExpanded> { extends BuildlessStreamNotifier<shelfsdk.LibraryItemExpanded> {
late final String id; late final String id;

View file

@ -1,11 +1,13 @@
import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref; import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref;
import 'package:logging/logging.dart' show Logger; import 'package:logging/logging.dart' show Logger;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'
import 'package:shelfsdk/audiobookshelf_api.dart' show Library; show GetLibrarysItemsReqParams, Library, LibraryItemMinified;
import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider; import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
import 'package:vaani/settings/api_settings_provider.dart' import 'package:vaani/features/settings/api_settings_provider.dart'
show apiSettingsProvider; show apiSettingsProvider;
import 'package:vaani/shared/extensions/model_conversions.dart';
part 'library_provider.g.dart'; part 'library_provider.g.dart';
final _logger = Logger('LibraryProvider'); final _logger = Logger('LibraryProvider');
@ -56,3 +58,27 @@ class Libraries extends _$Libraries {
return libraries; return libraries;
} }
} }
//
@riverpod
Future<List<LibraryItemMinified>> currentLibraryItems(Ref ref) async {
final api = ref.watch(authenticatedApiProvider);
final libraryId =
ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId));
if (libraryId == null) {
_logger.warning('No active library id found');
return [];
}
final items = await api.libraries.getItems(
libraryId: libraryId,
parameters: const GetLibrarysItemsReqParams(
limit: 18,
page: 1,
minified: true,
),
);
if (items == null) {
return [];
}
return items.results.map((v) => v.asMinified).toList();
}

View file

@ -173,6 +173,26 @@ final currentLibraryProvider = AutoDisposeFutureProvider<Library?>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef CurrentLibraryRef = AutoDisposeFutureProviderRef<Library?>; typedef CurrentLibraryRef = AutoDisposeFutureProviderRef<Library?>;
String _$currentLibraryItemsHash() =>
r'2e2ce270c46bedf0b779399772df89a23803fe50';
/// See also [currentLibraryItems].
@ProviderFor(currentLibraryItems)
final currentLibraryItemsProvider =
AutoDisposeFutureProvider<List<LibraryItemMinified>>.internal(
currentLibraryItems,
name: r'currentLibraryItemsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentLibraryItemsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef CurrentLibraryItemsRef
= AutoDisposeFutureProviderRef<List<LibraryItemMinified>>;
String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f'; String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f';
/// See also [Libraries]. /// See also [Libraries].

View file

@ -2,8 +2,9 @@ import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/api/authenticated_users_provider.dart';
import 'package:vaani/db/storage.dart'; import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model; import 'package:vaani/features/settings/models/audiobookshelf_server.dart'
as model;
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'server_provider.g.dart'; part 'server_provider.g.dart';

View file

@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/foundation.dart' show immutable;
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart'; import 'package:vaani/features/settings/models/models.dart';
@immutable @immutable
class AvailableHiveBoxes { class AvailableHiveBoxes {

View file

@ -1,5 +1,13 @@
class CacheKey { class CacheKey {
static String personalized(String id) {
return 'personalizedView:$id';
}
static String libraryItem(String id) { static String libraryItem(String id) {
return 'library_item_$id'; return 'library_item_$id';
} }
static String libraryItems(String id) {
return 'library_items_$id';
}
} }

View file

@ -13,7 +13,7 @@ Future initStorage() async {
// ); // );
// await storageDir.create(recursive: true); // await storageDir.create(recursive: true);
Hive.defaultDirectory = appStorageDir.path; Hive.defaultDirectory = appDocumentsDir.path;
appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}'); appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}');
await registerModels(); await registerModels();

View file

@ -1,6 +1,6 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart'; import 'package:vaani/features/settings/models/models.dart';
// register all models to Hive for serialization // register all models to Hive for serialization
Future registerModels() async { Future registerModels() async {

View file

@ -5,8 +5,8 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/globals.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
@ -72,11 +72,10 @@ class AudiobookDownloadManager {
) async { ) async {
_logger.info('queuing download for item: ${item.id}'); _logger.info('queuing download for item: ${item.id}');
// create a download task for each file in the item // create a download task for each file in the item
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
// check if the file is already downloaded // check if the file is already downloaded
if (isFileDownloaded( if (isFileDownloaded(
constructFilePath(directory, item, file), constructFilePath(item, file),
)) { )) {
_logger.info('file already downloaded: ${file.metadata.filename}'); _logger.info('file already downloaded: ${file.metadata.filename}');
continue; continue;
@ -102,11 +101,10 @@ class AudiobookDownloadManager {
} }
String constructFilePath( String constructFilePath(
Directory directory,
LibraryItemExpanded item, LibraryItemExpanded item,
LibraryFile file, LibraryFile file,
) => ) =>
'${directory.path}/${item.relPath}/${file.metadata.filename}'; '${appSupportDir.path}/${item.relPath}/${file.metadata.filename}';
void dispose() { void dispose() {
_updatesSubscription.cancel(); _updatesSubscription.cancel();
@ -125,10 +123,9 @@ class AudiobookDownloadManager {
Future<List<LibraryFile>> getDownloadedFilesMetadata( Future<List<LibraryFile>> getDownloadedFilesMetadata(
LibraryItemExpanded item, LibraryItemExpanded item,
) async { ) async {
final directory = await getApplicationSupportDirectory();
final downloadedFiles = <LibraryFile>[]; final downloadedFiles = <LibraryFile>[];
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file); final filePath = constructFilePath(item, file);
if (isFileDownloaded(filePath)) { if (isFileDownloaded(filePath)) {
downloadedFiles.add(file); downloadedFiles.add(file);
} }
@ -146,9 +143,8 @@ class AudiobookDownloadManager {
} }
Future<bool> isItemDownloaded(LibraryItemExpanded item) async { Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
if (!isFileDownloaded(constructFilePath(directory, item, file))) { if (!isFileDownloaded(constructFilePath(item, file))) {
_logger.info('file not downloaded: ${file.metadata.filename}'); _logger.info('file not downloaded: ${file.metadata.filename}');
return false; return false;
} }
@ -159,9 +155,8 @@ class AudiobookDownloadManager {
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async { Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.info('deleting downloaded item with id: ${item.id}'); _logger.info('deleting downloaded item with id: ${item.id}');
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file); final filePath = constructFilePath(item, file);
if (isFileDownloaded(filePath)) { if (isFileDownloaded(filePath)) {
File(filePath).deleteSync(); File(filePath).deleteSync();
} }
@ -170,10 +165,9 @@ class AudiobookDownloadManager {
} }
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async { Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async {
final directory = await getApplicationSupportDirectory();
final files = <Uri>[]; final files = <Uri>[];
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file); final filePath = constructFilePath(item, file);
if (isFileDownloaded(filePath)) { if (isFileDownloaded(filePath)) {
files.add(Uri.file(filePath)); files.add(Uri.file(filePath));
} }

View file

@ -6,7 +6,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/core/download_manager.dart' as core; import 'package:vaani/features/downloads/core/download_manager.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/item_files.dart'; import 'package:vaani/shared/extensions/item_files.dart';
part 'download_manager.g.dart'; part 'download_manager.g.dart';

View file

@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
part 'search_result_provider.g.dart'; part 'search_result_provider.g.dart';

View file

@ -13,8 +13,8 @@ import 'package:vaani/features/explore/providers/search_controller.dart';
import 'package:vaani/features/explore/view/search_result_page.dart'; import 'package:vaani/features/explore/view/search_result_page.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';

View file

@ -14,13 +14,12 @@ import 'package:vaani/features/downloads/providers/download_manager.dart'
isItemDownloadingProvider, isItemDownloadingProvider,
itemDownloadProgressProvider; itemDownloadProgressProvider;
import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart';
import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';

View file

@ -9,7 +9,7 @@ import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/router/models/library_item_extras.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/duration_format.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
@ -139,20 +139,21 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull; final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull;
if (libraryItem == null) { if (libraryItem == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final mediaProgress = libraryItem.userMediaProgress; final mediaProgress = libraryItem.userMediaProgress;
if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { if (mediaProgress == null &&
player.session?.libraryItemId != libraryItem.id) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
double progress; double progress;
Duration remainingTime; Duration remainingTime;
if (player.book?.libraryItemId == libraryItem.id) { if (player.session?.libraryItemId == libraryItem.id) {
// final positionStream = useStream(player.slowPositionStream); // final positionStream = useStream(player.slowPositionStream);
progress = (player.positionInBook).inSeconds / progress = (player.positionInBook).inSeconds /
libraryItem.media.asBookExpanded.duration.inSeconds; libraryItem.media.asBookExpanded.duration.inSeconds;

View file

@ -6,7 +6,7 @@ import 'package:vaani/shared/extensions/duration_format.dart';
Future<String> getLoggingFilePath() async { Future<String> getLoggingFilePath() async {
// final Directory directory = await getApplicationDocumentsDirectory(); // final Directory directory = await getApplicationDocumentsDirectory();
return '${appStorageDir.path}/$appName.log'; return '${appDocumentsDir.path}/$appName.log';
} }
Future<void> initLogging() async { Future<void> initLogging() async {

View file

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import '../models/flow.dart'; import '../models/flow.dart';

View file

@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/onboarding/providers/oauth_provider.dart'; import 'package:vaani/features/onboarding/providers/oauth_provider.dart';
import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; import 'package:vaani/features/onboarding/view/user_login_with_password.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
class CallbackPage extends HookConsumerWidget { class CallbackPage extends HookConsumerWidget {

View file

@ -6,7 +6,7 @@ import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/onboarding/view/user_login.dart'; import 'package:vaani/features/onboarding/view/user_login.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
import 'package:vaani/shared/widgets/add_new_server.dart'; import 'package:vaani/shared/widgets/add_new_server.dart';

View file

@ -17,10 +17,11 @@ import 'package:vaani/features/onboarding/view/user_login_with_token.dart'
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/hacks/fix_autofill_losing_focus.dart' import 'package:vaani/hacks/fix_autofill_losing_focus.dart'
show InactiveFocusScopeObserver; show InactiveFocusScopeObserver;
import 'package:vaani/models/error_response.dart' show ErrorResponseHandler; import 'package:vaani/shared/utils/error_response.dart'
import 'package:vaani/settings/api_settings_provider.dart' show ErrorResponseHandler;
import 'package:vaani/features/settings/api_settings_provider.dart'
show apiSettingsProvider; show apiSettingsProvider;
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/features/settings/models/models.dart' as model;
class UserLoginWidget extends HookConsumerWidget { class UserLoginWidget extends HookConsumerWidget {
const UserLoginWidget({ const UserLoginWidget({

View file

@ -7,9 +7,9 @@ import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/onboarding/providers/oauth_provider.dart'; import 'package:vaani/features/onboarding/providers/oauth_provider.dart';
import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; import 'package:vaani/features/onboarding/view/user_login_with_password.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/features/settings/models/models.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';

View file

@ -9,9 +9,9 @@ import 'package:vaani/api/authenticated_users_provider.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/features/settings/models/models.dart' as model;
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
class UserLoginWithPassword extends HookConsumerWidget { class UserLoginWithPassword extends HookConsumerWidget {

View file

@ -6,9 +6,9 @@ import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/api/authenticated_users_provider.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/shared/utils/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/features/settings/models/models.dart' as model;
class UserLoginWithToken extends HookConsumerWidget { class UserLoginWithToken extends HookConsumerWidget {
UserLoginWithToken({ UserLoginWithToken({

View file

@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/features/settings/models/app_settings.dart';
part 'nullable_player_settings.freezed.dart'; part 'nullable_player_settings.freezed.dart';
part 'nullable_player_settings.g.dart'; part 'nullable_player_settings.g.dart';

View file

@ -1,340 +0,0 @@
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
final _logger = Logger('PlaybackReporter');
/// this playback reporter will watch the player and report to the server
///
/// it will by default report every 10 seconds
/// and also report when the player is paused/stopped/finished/playing
class PlaybackReporter {
/// The player to watch
final AudiobookPlayer player;
/// the api to report to
final AudiobookshelfApi authenticatedApi;
/// The stopwatch to keep track of the time since the last report
///
/// this should only run when media is playing
final _stopwatch = Stopwatch();
/// subscriptions to listen and then cancel when disposing
final List<StreamSubscription> _subscriptions = [];
Duration _reportingInterval;
/// the duration to wait before reporting
Duration get reportingInterval => _reportingInterval;
set reportingInterval(Duration value) {
_reportingInterval = value;
_cancelReportTimer();
_setReportTimerIfNotAlready();
_logger.info('set interval: $value');
}
/// the minimum duration to report
final Duration reportingDurationThreshold;
/// the duration to wait before starting the reporting
/// this is to ignore the initial duration in case user is browsing
final Duration? minimumPositionForReporting;
/// the duration to mark the book as complete when the time left is less than this
final Duration markCompleteWhenTimeLeft;
/// timer to report every 10 seconds
/// tracking the time since the last report
Timer? _reportTimer;
/// metadata to report
String? deviceName;
String? deviceModel;
String? deviceSdkVersion;
String? deviceClientName;
String? deviceClientVersion;
String? deviceManufacturer;
PlaybackReporter(
this.player,
this.authenticatedApi, {
this.deviceName,
this.deviceModel,
this.deviceSdkVersion,
this.deviceClientName,
this.deviceClientVersion,
this.deviceManufacturer,
this.reportingDurationThreshold = const Duration(seconds: 1),
Duration reportingInterval = const Duration(seconds: 10),
this.minimumPositionForReporting,
this.markCompleteWhenTimeLeft = const Duration(seconds: 5),
}) : _reportingInterval = reportingInterval {
// initial conditions
if (player.playing) {
_stopwatch.start();
_setReportTimerIfNotAlready();
_logger.fine('starting stopwatch');
} else {
_logger.fine('not starting stopwatch');
}
_subscriptions.add(
player.playerStateStream.listen((state) async {
// set timer if any book is playing and cancel if not
if (player.book != null) {
if (state.playing) {
_setReportTimerIfNotAlready();
} else {
_cancelReportTimer();
}
} else if (player.book == null && _reportTimer != null) {
_logger.info('book is null, closing session');
await closeSession();
_cancelReportTimer();
}
// start or stop the stopwatch based on the playing state
if (state.playing) {
_stopwatch.start();
_logger.fine(
'player state observed, starting stopwatch at ${_stopwatch.elapsed}',
);
} else if (!state.playing) {
_stopwatch.stop();
_logger.fine(
'player state observed, stopping stopwatch at ${_stopwatch.elapsed}',
);
await tryReportPlayback(null);
}
}),
);
_logger.fine(
'initialized with reportingInterval: $reportingInterval, reportingDurationThreshold: $reportingDurationThreshold',
);
_logger.fine(
'initialized with minimumPositionForReporting: $minimumPositionForReporting, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft',
);
_logger.fine(
'initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer',
);
}
Future<void> tryReportPlayback(_) async {
_logger.fine(
'callback called when elapsed ${_stopwatch.elapsed}',
);
if (player.book != null &&
player.positionInBook >=
player.book!.duration - markCompleteWhenTimeLeft) {
_logger.info(
'marking complete as time left is less than $markCompleteWhenTimeLeft',
);
await markComplete();
return;
}
if (_stopwatch.elapsed > reportingDurationThreshold) {
_logger.fine(
'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold',
);
await syncCurrentPosition();
}
}
/// dispose the timer
Future<void> dispose() async {
for (var sub in _subscriptions) {
sub.cancel();
}
await closeSession();
_stopwatch.stop();
_reportTimer?.cancel();
_logger.fine('disposed');
}
/// current sessionId
/// this is used to report the playback
PlaybackSession? _session;
String? get sessionId => _session?.id;
Future<PlaybackSession?> startSession() async {
if (_session != null) {
return _session!;
}
if (player.book == null) {
_logger.warning('No audiobook playing to start session');
return null;
}
_session = await authenticatedApi.items.play(
libraryItemId: player.book!.libraryItemId,
parameters: PlayItemReqParams(
deviceInfo: await _getDeviceInfo(),
forceDirectPlay: false,
forceTranscode: false,
supportedMimeTypes: [
"audio/flac",
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/aac",
"audio/webm",
],
),
responseErrorHandler: _responseErrorHandler,
);
_logger.info('Started session: $sessionId');
return _session;
}
Future<void> markComplete() async {
if (player.book == null) {
throw NoAudiobookPlayingError();
}
await authenticatedApi.me.createUpdateMediaProgress(
libraryItemId: player.book!.libraryItemId,
parameters: CreateUpdateProgressReqParams(
isFinished: true,
currentTime: player.positionInBook,
duration: player.book!.duration,
),
responseErrorHandler: _responseErrorHandler,
);
_logger.info('Marked complete for book: ${player.book!.libraryItemId}');
}
Future<void> syncCurrentPosition() async {
final data = _getSyncData();
if (data == null) {
await closeSession();
}
try {
_session ??= await startSession();
} on Error catch (e) {
_logger.warning('Error starting session: $e');
}
if (_session == null) {
_logger.warning('No session to sync position');
return;
}
final currentPosition = player.positionInBook;
await authenticatedApi.sessions.syncOpen(
sessionId: sessionId!,
parameters: _getSyncData()!,
responseErrorHandler: _responseErrorHandler,
);
_logger.fine(
'Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId',
);
// reset the stopwatch
_stopwatch.reset();
}
Future<void> closeSession() async {
if (sessionId == null) {
_logger.warning('No session to close');
return;
}
await authenticatedApi.sessions.closeOpen(
sessionId: sessionId!,
parameters: _getSyncData(),
responseErrorHandler: _responseErrorHandler,
);
_session = null;
_logger.info('Closed session');
}
void _setReportTimerIfNotAlready() {
if (_reportTimer != null) return;
_reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback);
_logger.fine('set timer with interval: $_reportingInterval');
}
void _cancelReportTimer() {
_reportTimer?.cancel();
_reportTimer = null;
_logger.fine('cancelled timer');
}
void _responseErrorHandler(http.Response response, [error]) {
if (response.statusCode != 200) {
_logger.severe('Error with api: ${response.obfuscate()}, $error');
throw PlaybackSyncError(
'Error syncing position: ${response.body}, $error',
);
}
}
SyncSessionReqParams? _getSyncData() {
if (player.book?.libraryItemId != _session?.libraryItemId) {
_logger.info(
'Book changed, not syncing position for session: $sessionId',
);
return null;
}
// if in the ignore duration, don't sync
if (minimumPositionForReporting != null &&
player.positionInBook < minimumPositionForReporting!) {
// but if elapsed time is more than the minimumPositionForReporting, sync
if (_stopwatch.elapsed > minimumPositionForReporting!) {
_logger.info(
'Syncing position despite being less than minimumPositionForReporting as elapsed time is more: ${_stopwatch.elapsed}',
);
} else {
_logger.info(
'Ignoring sync for position: ${player.positionInBook} < $minimumPositionForReporting',
);
return null;
}
}
return SyncSessionReqParams(
currentTime: player.positionInBook,
timeListened: _stopwatch.elapsed,
duration: player.book?.duration ?? Duration.zero,
);
}
Future<DeviceInfoReqParams> _getDeviceInfo() async {
return DeviceInfoReqParams(
clientVersion: deviceClientVersion,
manufacturer: deviceManufacturer,
model: deviceModel,
sdkVersion: deviceSdkVersion,
clientName: deviceClientName,
deviceName: deviceName,
);
}
}
class PlaybackSyncError implements Exception {
String message;
PlaybackSyncError([this.message = 'Error syncing playback']);
@override
String toString() {
return 'PlaybackSyncError: $message';
}
}
class NoAudiobookPlayingError implements Exception {
String message;
NoAudiobookPlayingError([this.message = 'No audiobook is playing']);
@override
String toString() {
return 'NoAudiobookPlayingError: $message';
}
}

View file

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/audiobook_player_session.dart'; import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
final _logger = Logger('PlaybackReporter'); final _logger = Logger('PlaybackReporter');

View file

@ -1,19 +1,22 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/playback_reporting/core/playback_reporter.dart' import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart'
as core; as core;
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart';
part 'playback_reporter_provider.g.dart'; part 'playback_reporter_provider.g.dart';
@Riverpod(keepAlive: true) @riverpod
class PlaybackReporter extends _$PlaybackReporter { class PlaybackReporter extends _$PlaybackReporter {
@override @override
Future<core.PlaybackReporter> build() async { Future<core.PlaybackReporter?> build() async {
final session = ref.watch(sessionProvider);
if (session == null) {
return null;
}
final playerSettings = ref.watch(appSettingsProvider).playerSettings; final playerSettings = ref.watch(appSettingsProvider).playerSettings;
final player = ref.watch(simpleAudiobookPlayerProvider); final player = ref.watch(playerProvider);
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final reporter = core.PlaybackReporter( final reporter = core.PlaybackReporter(
@ -22,12 +25,7 @@ class PlaybackReporter extends _$PlaybackReporter {
reportingInterval: playerSettings.playbackReportInterval, reportingInterval: playerSettings.playbackReportInterval,
markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft,
minimumPositionForReporting: playerSettings.minimumPositionForReporting, minimumPositionForReporting: playerSettings.minimumPositionForReporting,
deviceName: deviceName, session: session,
deviceModel: deviceModel,
deviceSdkVersion: deviceSdkVersion,
deviceClientName: appName,
deviceClientVersion: appVersion,
deviceManufacturer: deviceManufacturer,
); );
ref.onDispose(reporter.dispose); ref.onDispose(reporter.dispose);
return reporter; return reporter;

View file

@ -6,12 +6,12 @@ part of 'playback_reporter_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$playbackReporterHash() => r'43bde2ac163830b6950303a80cdd915ffcb1943b'; String _$playbackReporterHash() => r'f9be5d6e4b07815ec669406cede4b00d2278e3af';
/// See also [PlaybackReporter]. /// See also [PlaybackReporter].
@ProviderFor(PlaybackReporter) @ProviderFor(PlaybackReporter)
final playbackReporterProvider = final playbackReporterProvider = AutoDisposeAsyncNotifierProvider<
AsyncNotifierProvider<PlaybackReporter, core.PlaybackReporter>.internal( PlaybackReporter, core.PlaybackReporter?>.internal(
PlaybackReporter.new, PlaybackReporter.new,
name: r'playbackReporterProvider', name: r'playbackReporterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -21,6 +21,6 @@ final playbackReporterProvider =
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$PlaybackReporter = AsyncNotifier<core.PlaybackReporter>; typedef _$PlaybackReporter = AutoDisposeAsyncNotifier<core.PlaybackReporter?>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,240 +1,239 @@
/// a wrapper around the audioplayers package to manage the audio player instance // my_audio_handler.dart
/// import 'dart:io';
/// this is needed as audiobook can be a list of audio files instead of a single file
library;
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
// import 'package:just_audio_background/just_audio_background.dart'; import 'package:rxdart/rxdart.dart';
import 'package:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/player_status.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
final _logger = Logger('AudiobookPlayer');
// add a small offset so the display does not show the previous chapter for a split second // add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10); final offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final doNotSeekBackIfLessThan = Duration(seconds: 5); final AudioPlayer _player = AudioPlayer();
// final List<AudioSource> _playlist = [];
final Ref ref;
/// returns the sum of the duration of all the previous tracks before the [index] PlaybackSessionExpanded? _session;
Duration sumOfTracks(BookExpanded book, int? index) {
_logger.fine('Calculating sum of tracks for index: $index');
// return 0 if index is less than 0
if (index == null || index < 0) {
_logger.warning('Index is null or less than 0, returning 0');
return Duration.zero;
}
final total = book.tracks.sublist(0, index).fold<Duration>(
Duration.zero,
(previousValue, element) => previousValue + element.duration,
);
_logger.fine('Sum of tracks for index: $index is $total');
return total;
}
/// will manage the audio player instance final _currentChapterObject = BehaviorSubject<BookChapter?>.seeded(null);
class AudiobookPlayer extends AudioPlayer { AbsAudioHandler(this.ref) {
// constructor which takes in the BookExpanded object _setupAudioPlayer();
AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book
_logger.config('Setting up audiobook player');
// playerStateStream.listen((playerState) {
// if (playerState.processingState == ProcessingState.completed) {
// Future.microtask(seekToNext);
// }
// });
} }
/// the [BookExpanded] being played void _setupAudioPlayer() {
BookExpanded? _book; final statusNotifier = ref.read(playerStatusProvider.notifier);
// /// the [BookExpanded] trying to be played //
// BookExpanded? _intended_book; _player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_player.playerStateStream.listen((event) {
/// the [BookExpanded] being played if (event.playing) {
/// statusNotifier.setPlayStatusVerify(core.PlayStatus.playing);
/// to set the book, use [setSourceAudiobook] } else {
BookExpanded? get book => _book; statusNotifier.setPlayStatusVerify(core.PlayStatus.paused);
}
/// the authentication token to access the [AudioTrack.contentUrl] });
final String token; _player.positionStream.distinct().listen((position) {
final chapter = _session?.findChapterAtTime(positionInBook);
/// the base url for the audio files if (chapter != currentChapter) {
final Uri baseUrl; _currentChapterObject.sink.add(chapter);
}
// the current index of the audio file in the [book]
// int _currentIndex = 0;
// available audio tracks
int? get availableTracks => _book?.tracks.length;
/// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudiobook(
BookExpanded? book, {
bool preload = true,
int? initialIndex,
Duration? initialPosition,
List<Uri>? downloadedUris,
Uri? artworkUri,
}) async {
_logger.finer(
'Initial position: $initialPosition, Downloaded URIs: $downloadedUris',
);
final appSettings = loadOrCreateAppSettings();
if (book == null) {
_book = null;
_logger.info('Book is null, stopping player');
return stop();
}
if (_book == book) {
_logger.info('Book is the same, doing nothing');
return;
}
_logger.info('Setting source for book: $book');
_logger.fine('Stopping player');
await stop();
_book = book;
// some calculations to set the initial index and position
// initialPosition is of the entire book not just the current track
// hence first we need to calculate the current track which will be used to set the initial position
// then we set the initial index to the current track index and position as the remaining duration from the position
// after subtracting the duration of all the previous tracks
// initialPosition ;
final trackToPlay =
_book!.findTrackAtTime(initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null
? initialPosition - trackToPlay.startOffset
: null;
_logger.finer('Setting audioSource');
final playlist = book.tracks.map((track) {
final retrievedUri =
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
// _logger.fine(
// 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
// );
return AudioSource.uri(
retrievedUri,
// tag: MediaItem(
// // Specify a unique ID for each media item:
// id: book.libraryItemId + track.index.toString(),
// // Metadata to display in the notification:
// title: appSettings.notificationSettings.primaryTitle
// .formatNotificationTitle(book),
// album: appSettings.notificationSettings.secondaryTitle
// .formatNotificationTitle(book),
// artUri: artworkUri ??
// Uri.parse(
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
// ),
// ),
);
}).toList();
await setAudioSources(
playlist,
preload: preload,
initialIndex: initialIndex,
initialPosition: initialPositionInTrack,
).catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
}); });
} }
/// toggles the player between play and pause //
Future<void> togglePlayPause() { Future<void> setSourceAudiobook(
// check if book is set PlaybackSessionExpanded playbackSession, {
if (_book == null) { required Uri baseUrl,
_logger.warning('No book is set, not toggling play/pause'); required String token,
List<Uri>? downloadedUris,
}) async {
_session = playbackSession;
//
List<AudioSource> audioSources = [];
for (final track in playbackSession.audioTracks) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
),
);
} }
// TODO refactor this to cover all the states playMediaItem(
return switch (playerState) { MediaItem(
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), id: playbackSession.libraryItemId,
}; album: playbackSession.mediaMetadata.title,
title: playbackSession.displayTitle,
displaySubtitle: playbackSession.mediaType == MediaType.book
? (playbackSession.mediaMetadata as BookMetadata).subtitle
: null,
duration: playbackSession.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
),
),
);
final track = playbackSession.findTrackAtTime(playbackSession.currentTime);
final index = playbackSession.audioTracks.indexOf(track);
await _player.setAudioSources(
audioSources,
initialIndex: index,
initialPosition: playbackSession.currentTime - track.startOffset,
);
_player.seek(playbackSession.currentTime - track.startOffset, index: index);
await play();
//
// if (initialPosition != null) {
// await seekInBook(initialPosition);
// }
} }
/// need to override getDuration and getCurrentPosition to return according to the book instead of the current track // //
/// this is because the book can be a list of audio files and the player is only aware of the current track // void _onTrackChanged(int trackIndex) {
/// so we need to calculate the duration and current position based on the book // if (_book == null) return;
Future<void> seekInBook(Duration globalPosition) async {
if (_book == null) { // //
_logger.warning('No book is set, not seeking'); // // print('切换到音轨: ${_book!.tracks[trackIndex].title}');
return; // }
}
//
final track = _book!.findTrackAtTime(globalPosition);
final index = _book!.tracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) {
positionInTrack = offset;
}
//
if (index != currentIndex) {
await seek(positionInTrack, index: index);
}
await seek(positionInTrack);
}
// //
Future<void> skipToChapter(int chapterId, {Duration? position}) async { Future<void> skipToChapter(int chapterId) async {
if (_book == null) return; if (_session == null) return;
final chapter = _book!.chapters.firstWhere( final chapter = _session!.chapters.firstWhere(
(ch) => ch.id == chapterId, (ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'), orElse: () => throw Exception('Chapter not found'),
); );
if (position != null) {
print('章节开头: ${chapter.start}');
print('章节开头: ${chapter.start + position}');
await seekInBook(chapter.start + position);
return;
}
await seekInBook(chapter.start + offset); await seekInBook(chapter.start + offset);
} }
PlaybackSessionExpanded? get session => _session;
//
AudioTrack? get currentTrack {
if (_session == null || _player.currentIndex == null) {
return null;
}
return _session!.audioTracks[_player.currentIndex!];
}
//
BookChapter? get currentChapter {
return _currentChapterObject.value;
}
Duration get position => _player.position;
Duration get positionInChapter {
return _player.position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
}
Duration get positionInBook {
return _player.position + (currentTrack?.startOffset ?? Duration.zero);
}
Duration get bufferedPositionInBook {
return _player.bufferedPosition +
(currentTrack?.startOffset ?? Duration.zero);
}
Duration? get chapterDuration => currentChapter?.duration;
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInBook {
return _player.positionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = _player.createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
);
return superPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return _player.bufferedPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.distinct().map((position) {
return position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
});
}
Stream<BookChapter?> get chapterStream => _currentChapterObject.stream;
Future<void> togglePlayPause() async {
// check if book is set
if (_session == null) {
return Future.value();
}
_player.playerState.playing ? await pause() : await play();
}
//
@override @override
Future<void> seekToNext() async { Future<void> play() async {
if (_book == null) { await _player.play();
}
@override
Future<void> pause() async {
await _player.pause();
}
// /
@override
Future<void> skipToNext() async {
if (_session == null) {
// 退 // 退
return super.seekToNext(); return _player.seekToNext();
} }
final chapter = currentChapter; final chapter = currentChapter;
if (chapter == null) { if (chapter == null) {
// 退 // 退
return super.seekToNext(); return _player.seekToNext();
} }
final currentIndex = _book!.chapters.indexOf(chapter); final chapterIndex = _session!.chapters.indexOf(chapter);
if (currentIndex < _book!.chapters.length - 1) { if (chapterIndex < _session!.chapters.length - 1) {
// //
final nextChapter = _book!.chapters[currentIndex + 1]; final nextChapter = _session!.chapters[chapterIndex + 1];
await skipToChapter(nextChapter.id); await skipToChapter(nextChapter.id);
} }
} }
@override @override
Future<void> seekToPrevious() async { Future<void> skipToPrevious() async {
if (_book == null) {
return super.seekToPrevious();
}
final chapter = currentChapter; final chapter = currentChapter;
if (chapter == null) { if (_session == null || chapter == null) {
return super.seekToPrevious(); return _player.seekToPrevious();
} }
final currentIndex = _book!.chapters.indexOf(chapter); final currentIndex = _session!.chapters.indexOf(chapter);
if (currentIndex > 0) { if (currentIndex > 0) {
// //
final prevChapter = _book!.chapters[currentIndex - 1]; final prevChapter = _session!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id); await skipToChapter(prevChapter.id);
} else { } else {
// //
@ -242,84 +241,77 @@ class AudiobookPlayer extends AudioPlayer {
} }
} }
/// a convenience method to get position in the book instead of the current track position @override
Duration get positionInBook { Future<void> seek(Duration position) async {
if (_book == null || currentIndex == null) { // position 使
return Duration.zero; //
final track = currentTrack;
Duration startOffset = Duration.zero;
if (track != null) {
startOffset = track.startOffset;
} }
return position + _book!.tracks[currentIndex!].startOffset; await seekInBook(startOffset + position);
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
} }
/// a convenience method to get the buffered position in the book instead of the current track position Future<void> setVolume(double volume) async {
Duration get bufferedPositionInBook { await _player.setVolume(volume);
if (_book == null || currentIndex == null) { }
return Duration.zero;
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
//
Future<void> seekInBook(Duration globalPosition) async {
if (_session == null) return;
//
final track = _session!.findTrackAtTime(globalPosition);
final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack < Duration.zero) {
positionInTrack = Duration.zero;
} }
return bufferedPosition + _book!.tracks[currentIndex!].startOffset; //
// return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset; await _player.seek(positionInTrack, index: index);
} }
// AudioPlayer get player => _player;
Stream<Duration> get positionStreamInChapter { PlaybackState _transformEvent(PlaybackEvent event) {
return super.positionStream.map((position) { return PlaybackState(
if (_book == null || currentIndex == null) { controls: [
return Duration.zero; if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious,
} MediaControl.rewind,
final globalPosition = if (_player.playing) MediaControl.pause else MediaControl.play,
position + _book!.tracks[currentIndex!].startOffset; MediaControl.stop,
final chapter = _book!.findChapterAtTime(globalPosition); MediaControl.fastForward,
return globalPosition - chapter.start; if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext,
}); ],
} systemActions: {
if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious,
/// streams to override to suit the book instead of the current track MediaAction.rewind,
// - positionStream MediaAction.seek,
// - bufferedPositionStream MediaAction.fastForward,
Stream<Duration> get positionStreamInBook { MediaAction.stop,
// return the positionInBook stream MediaAction.setSpeed,
return super.positionStream.map((position) { if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext,
if (_book == null || currentIndex == null) { },
return Duration.zero; androidCompactActionIndices: const [1, 2, 3],
} processingState: const {
return position + _book!.tracks[currentIndex!].startOffset; ProcessingState.idle: AudioProcessingState.idle,
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; ProcessingState.loading: AudioProcessingState.loading,
}); ProcessingState.buffering: AudioProcessingState.buffering,
} ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
Stream<Duration> get bufferedPositionStreamInBook { }[_player.processingState] ??
return super.bufferedPositionStream.map((position) { AudioProcessingState.idle,
if (_book == null || currentIndex == null) { playing: _player.playing,
return Duration.zero; updatePosition: _player.position,
} bufferedPosition: event.bufferedPosition,
return position + _book!.tracks[currentIndex!].startOffset; speed: _player.speed,
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset; queueIndex: event.currentIndex,
}); captioningEnabled: false,
}
/// a convenience getter for slow position stream
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
); );
// now we need to map the position to the book instead of the current track
return superPositionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
/// get current chapter
BookChapter? get currentChapter {
if (_book == null) {
return null;
}
return _book!.findChapterAtTime(positionInBook);
} }
} }
@ -340,46 +332,7 @@ Uri _getUri(
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
} }
extension FormatNotificationTitle on String { extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
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) ??
'';
},
);
}
}
extension NotificationTitleUtils on NotificationTitleType {
String? extractFrom(BookExpanded book) {
var bookMetadataExpanded = book.metadata.asBookMetadataExpanded;
switch (this) {
case NotificationTitleType.bookTitle:
return bookMetadataExpanded.title;
case NotificationTitleType.chapterTitle:
// TODO: implement chapter title; depends on https://github.com/Dr-Blank/Vaani/issues/2
return bookMetadataExpanded.title;
case NotificationTitleType.author:
return bookMetadataExpanded.authorName;
case NotificationTitleType.narrator:
return bookMetadataExpanded.narratorName;
case NotificationTitleType.series:
return bookMetadataExpanded.seriesName;
case NotificationTitleType.subtitle:
return bookMetadataExpanded.subtitle;
case NotificationTitleType.year:
return bookMetadataExpanded.publishedYear;
}
}
}
extension BookExpandedExtension on BookExpanded {
BookChapter findChapterAtTime(Duration position) { BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere( return chapters.firstWhere(
(element) { (element) {
@ -390,16 +343,23 @@ extension BookExpandedExtension on BookExpanded {
} }
AudioTrack findTrackAtTime(Duration position) { AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere( return audioTracks.firstWhere(
(element) { (element) {
return element.startOffset <= position && return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset; element.startOffset + element.duration >= position + offset;
}, },
orElse: () => tracks.first, orElse: () => audioTracks.first,
); );
} }
int findTrackIndexAtTime(Duration position) {
return audioTracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) { Duration getTrackStartOffset(int index) {
return tracks[index].startOffset; return audioTracks[index].startOffset;
} }
} }

View file

@ -1,365 +0,0 @@
// my_audio_handler.dart
import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/player_status.dart' as core;
import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/shared/extensions/chapter.dart';
// add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10);
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AudioPlayer _player = AudioPlayer();
// final List<AudioSource> _playlist = [];
final Ref ref;
PlaybackSessionExpanded? _session;
final _currentChapterObject = BehaviorSubject<BookChapter?>.seeded(null);
AbsAudioHandler(this.ref) {
_setupAudioPlayer();
}
void _setupAudioPlayer() {
final statusNotifier = ref.read(playerStatusProvider.notifier);
//
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_player.playerStateStream.listen((event) {
if (event.playing) {
statusNotifier.setPlayStatusVerify(core.PlayStatus.playing);
} else {
statusNotifier.setPlayStatusVerify(core.PlayStatus.paused);
}
});
_player.positionStream.distinct().listen((position) {
final chapter = _session?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) {
_currentChapterObject.sink.add(chapter);
}
});
}
//
Future<void> setSourceAudiobook(
PlaybackSessionExpanded playbackSession, {
required Uri baseUrl,
required String token,
List<Uri>? downloadedUris,
}) async {
_session = playbackSession;
//
List<AudioSource> audioSources = [];
for (final track in playbackSession.audioTracks) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
),
);
}
playMediaItem(
MediaItem(
id: playbackSession.libraryItemId,
album: playbackSession.mediaMetadata.title,
title: playbackSession.displayTitle,
displaySubtitle: playbackSession.mediaType == MediaType.book
? (playbackSession.mediaMetadata as BookMetadata).subtitle
: null,
duration: playbackSession.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
),
),
);
final track = playbackSession.findTrackAtTime(playbackSession.currentTime);
final index = playbackSession.audioTracks.indexOf(track);
await _player.setAudioSources(
audioSources,
initialIndex: index,
initialPosition: playbackSession.currentTime - track.startOffset,
);
_player.seek(playbackSession.currentTime - track.startOffset, index: index);
await play();
//
// if (initialPosition != null) {
// await seekInBook(initialPosition);
// }
}
// //
// void _onTrackChanged(int trackIndex) {
// if (_book == null) return;
// //
// // print('切换到音轨: ${_book!.tracks[trackIndex].title}');
// }
//
Future<void> skipToChapter(int chapterId) async {
if (_session == null) return;
final chapter = _session!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
await seekInBook(chapter.start + offset);
}
PlaybackSessionExpanded? get session => _session;
//
AudioTrack? get currentTrack {
if (_session == null || _player.currentIndex == null) {
return null;
}
return _session!.audioTracks[_player.currentIndex!];
}
//
BookChapter? get currentChapter {
return _currentChapterObject.value;
}
Duration get position => _player.position;
Duration get positionInChapter {
return _player.position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
}
Duration get positionInBook {
return _player.position + (currentTrack?.startOffset ?? Duration.zero);
}
Duration get bufferedPositionInBook {
return _player.bufferedPosition +
(currentTrack?.startOffset ?? Duration.zero);
}
Duration? get chapterDuration => currentChapter?.duration;
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInBook {
return _player.positionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = _player.createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
);
return superPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return _player.bufferedPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.distinct().map((position) {
return position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
});
}
Stream<BookChapter?> get chapterStream => _currentChapterObject.stream;
Future<void> togglePlayPause() async {
// check if book is set
if (_session == null) {
return Future.value();
}
_player.playerState.playing ? await pause() : await play();
}
//
@override
Future<void> play() async {
await _player.play();
}
@override
Future<void> pause() async {
await _player.pause();
}
// /
@override
Future<void> skipToNext() async {
if (_session == null) {
// 退
return _player.seekToNext();
}
final chapter = currentChapter;
if (chapter == null) {
// 退
return _player.seekToNext();
}
final chapterIndex = _session!.chapters.indexOf(chapter);
if (chapterIndex < _session!.chapters.length - 1) {
//
final nextChapter = _session!.chapters[chapterIndex + 1];
await skipToChapter(nextChapter.id);
}
}
@override
Future<void> skipToPrevious() async {
final chapter = currentChapter;
if (_session == null || chapter == null) {
return _player.seekToPrevious();
}
final currentIndex = _session!.chapters.indexOf(chapter);
if (currentIndex > 0) {
//
final prevChapter = _session!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id);
} else {
//
await seekInBook(Duration.zero);
}
}
@override
Future<void> seek(Duration position) async {
// position 使
//
final track = currentTrack;
Duration startOffset = Duration.zero;
if (track != null) {
startOffset = track.startOffset;
}
await seekInBook(startOffset + position);
}
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
//
Future<void> seekInBook(Duration globalPosition) async {
if (_session == null) return;
//
final track = _session!.findTrackAtTime(globalPosition);
final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack < Duration.zero) {
positionInTrack = Duration.zero;
}
//
await _player.seek(positionInTrack, index: index);
}
AudioPlayer get player => _player;
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious,
MediaControl.rewind,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.fastForward,
if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext,
],
systemActions: {
if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious,
MediaAction.rewind,
MediaAction.seek,
MediaAction.fastForward,
MediaAction.stop,
MediaAction.setSpeed,
if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext,
},
androidCompactActionIndices: const [1, 2, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState] ??
AudioProcessingState.idle,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: event.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
captioningEnabled: false,
);
}
}
Uri _getUri(
AudioTrack track,
List<Uri>? downloadedUris, {
required Uri baseUrl,
required String token,
}) {
// check if the track is in the downloadedUris
final uri = downloadedUris?.firstWhereOrNull(
(element) {
return element.pathSegments.last == track.metadata?.filename;
},
);
return uri ??
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
}
extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return audioTracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => audioTracks.first,
);
}
int findTrackIndexAtTime(Duration position) {
return audioTracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) {
return audioTracks[index].startOffset;
}
}

View file

@ -1,62 +0,0 @@
// import 'package:audio_service/audio_service.dart';
// import 'package:audio_session/audio_session.dart';
// import 'package:just_audio_background/just_audio_background.dart'
// show JustAudioBackground, NotificationConfig;
// import 'package:just_audio_media_kit/just_audio_media_kit.dart'
// show JustAudioMediaKit;
// import 'package:vaani/settings/app_settings_provider.dart';
// import 'package:vaani/settings/models/app_settings.dart';
// Future<void> configurePlayer() async {
// // for playing audio on windows, linux
// JustAudioMediaKit.ensureInitialized(windows: false);
// // for configuring how this app will interact with other audio apps
// final session = await AudioSession.instance;
// await session.configure(const AudioSessionConfiguration.speech());
// final appSettings = loadOrCreateAppSettings();
// // for playing audio in the background
// await JustAudioBackground.init(
// androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio',
// androidNotificationChannelName: 'Audio playback',
// androidNotificationOngoing: false,
// androidStopForegroundOnPause: false,
// androidNotificationChannelDescription: 'Audio playback in the background',
// androidNotificationIcon: 'drawable/ic_stat_logo',
// rewindInterval: appSettings.notificationSettings.rewindInterval,
// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
// androidShowNotificationBadge: false,
// notificationConfigBuilder: (state) {
// final controls = [
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.skipToPreviousChapter) &&
// state.hasPrevious)
// MediaControl.skipToPrevious,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.rewind))
// MediaControl.rewind,
// if (state.playing) MediaControl.pause else MediaControl.play,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.fastForward))
// MediaControl.fastForward,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.skipToNextChapter) &&
// state.hasNext)
// MediaControl.skipToNext,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.stop))
// MediaControl.stop,
// ];
// return NotificationConfig(
// controls: controls,
// systemActions: const {
// MediaAction.seek,
// MediaAction.seekForward,
// MediaAction.seekBackward,
// },
// );
// },
// );
// }

View file

@ -1,71 +1,112 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:audio_service/audio_service.dart';
import 'package:logging/logging.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:shelfsdk/audiobookshelf_api.dart' as core;
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/player/core/audiobook_player.dart' as core; import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/utils/helper.dart';
part 'audiobook_player.g.dart'; part 'audiobook_player.g.dart';
final _logger = Logger('AudiobookPlayerProvider');
const playerId = 'audiobook_player';
/// Simple because it doesn't rebuild when the player state changes
/// it only rebuilds when the token changes
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer { Future<AbsAudioHandler> audioHandlerInit(Ref ref) async {
if (Helper.isWindows() || Helper.isLinux()) {
// JustAudioMediaKit.ensureInitialized(windows: false);
JustAudioMediaKit.ensureInitialized();
}
final audioService = await AudioService.init(
builder: () => AbsAudioHandler(ref),
config: const AudioServiceConfig(
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
androidNotificationChannelName: 'ABSPlayback',
androidNotificationChannelDescription:
'Needed to control audio from lock screen',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
androidNotificationIcon: 'drawable/ic_stat_logo',
preloadArtwork: true,
),
);
return audioService;
}
@Riverpod(keepAlive: true)
class Player extends _$Player {
@override @override
core.AudiobookPlayer build() { AbsAudioHandler build() {
final api = ref.watch(authenticatedApiProvider); return ref.watch(audioHandlerInitProvider).requireValue;
final player = core.AudiobookPlayer(
api.token!,
api.baseUrl,
);
ref.onDispose(player.dispose);
_logger.finer('created simple player');
return player;
} }
} }
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class AudiobookPlayer extends _$AudiobookPlayer { class Session extends _$Session {
@override @override
core.AudiobookPlayer build() { core.PlaybackSessionExpanded? build() {
final player = ref.watch(simpleAudiobookPlayerProvider); return null;
ref.onDispose(player.dispose);
// bind notify listeners to the player
// player.playerStateStream.listen((_) {
// ref.notifyListeners();
// });
_logger.finer('created player');
return player;
} }
Future<void> setSpeed(double speed) async { Future<void> load(String id, String? episodeId) async {
await state.setSpeed(speed); final audioService = ref.read(playerProvider);
ref.notifyListeners(); await audioService.pause();
} ref.read(playerStatusProvider.notifier).setLoading(id);
final api = ref.read(authenticatedApiProvider);
final playBack = await ref.watch(playBackSessionProvider(id).future);
if (playBack == null) {
return;
}
state = playBack.asExpanded;
final downloadManager = ref.read(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(state!.libraryItemId).future);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
Future<void> setSourceAudiobook({ var bookPlayerSettings =
required shelfsdk.BookExpanded book, ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings;
shelfsdk.MediaProgress? userMediaProgress, var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
}) async {
ref.notifyListeners(); var configurePlayerForEveryBook =
appPlayerSettings.configurePlayerForEveryBook;
await Future.wait([
audioService.setSourceAudiobook(
state!.asExpanded,
baseUrl: api.baseUrl,
token: api.token!,
downloadedUris: downloadedUris,
),
// set the volume
audioService.setVolume(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
),
// set the speed
audioService.setSpeed(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
),
]);
} }
} }
@riverpod class PlaybackSyncError implements Exception {
bool isPlayerPlaying( String message;
Ref ref,
) { PlaybackSyncError([this.message = 'Error syncing playback']);
final player = ref.watch(audiobookPlayerProvider);
print("playing: ${player.playing}"); @override
return player.playing; String toString() {
return 'PlaybackSyncError: $message';
}
} }

View file

@ -6,58 +6,51 @@ part of 'audiobook_player.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$isPlayerPlayingHash() => r'b81fa9cfb51c88c8d9e8f5c1f4f6a12d9e5a0cc1'; String _$audioHandlerInitHash() => r'6e4662a45c1c6e84aa16436f71ffcfecc3d4bdab';
/// See also [isPlayerPlaying]. /// See also [audioHandlerInit].
@ProviderFor(isPlayerPlaying) @ProviderFor(audioHandlerInit)
final isPlayerPlayingProvider = AutoDisposeProvider<bool>.internal( final audioHandlerInitProvider = FutureProvider<AbsAudioHandler>.internal(
isPlayerPlaying, audioHandlerInit,
name: r'isPlayerPlayingProvider', name: r'audioHandlerInitProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$isPlayerPlayingHash, : _$audioHandlerInitHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef IsPlayerPlayingRef = AutoDisposeProviderRef<bool>; typedef AudioHandlerInitRef = FutureProviderRef<AbsAudioHandler>;
String _$simpleAudiobookPlayerHash() => String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f';
r'5e94bbff4314adceb5affa704fc4d079d4016afa';
/// Simple because it doesn't rebuild when the player state changes /// See also [Player].
/// it only rebuilds when the token changes @ProviderFor(Player)
/// final playerProvider = NotifierProvider<Player, AbsAudioHandler>.internal(
/// Copied from [SimpleAudiobookPlayer]. Player.new,
@ProviderFor(SimpleAudiobookPlayer) name: r'playerProvider',
final simpleAudiobookPlayerProvider = debugGetCreateSourceHash:
NotifierProvider<SimpleAudiobookPlayer, core.AudiobookPlayer>.internal( const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash,
SimpleAudiobookPlayer.new,
name: r'simpleAudiobookPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleAudiobookPlayerHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$SimpleAudiobookPlayer = Notifier<core.AudiobookPlayer>; typedef _$Player = Notifier<AbsAudioHandler>;
String _$audiobookPlayerHash() => r'04448247e79c5d60b9fd6f98eeeb865f1e8d0ff8'; String _$sessionHash() => r'c171809249c3021dc445dc1ba90fe8626a3d3b54';
/// See also [AudiobookPlayer]. /// See also [Session].
@ProviderFor(AudiobookPlayer) @ProviderFor(Session)
final audiobookPlayerProvider = final sessionProvider =
NotifierProvider<AudiobookPlayer, core.AudiobookPlayer>.internal( NotifierProvider<Session, core.PlaybackSessionExpanded?>.internal(
AudiobookPlayer.new, Session.new,
name: r'audiobookPlayerProvider', name: r'sessionProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash:
? null const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash,
: _$audiobookPlayerHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$AudiobookPlayer = Notifier<core.AudiobookPlayer>; typedef _$Session = Notifier<core.PlaybackSessionExpanded?>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,46 +1,40 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as core;
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
part 'currently_playing_provider.g.dart'; part 'currently_playing_provider.g.dart';
final _logger = Logger('CurrentlyPlayingProvider');
@riverpod @riverpod
BookExpanded? currentlyPlayingBook(Ref ref) { class CurrentChapter extends _$CurrentChapter {
try { @override
final book = ref.watch(simpleAudiobookPlayerProvider.select((v) => v.book)); core.BookChapter? build() {
return book; final player = ref.watch(playerProvider);
} catch (e) { player.chapterStream.distinct().listen((chapter) {
_logger.warning('Error getting currently playing book: $e'); update(chapter);
return null; });
return player.currentChapter;
}
void update(core.BookChapter? chapter) {
if (state != chapter) {
state = chapter;
}
} }
} }
/// provided the current chapter of the book being played
@riverpod @riverpod
BookChapter? currentPlayingChapter(Ref ref) { List<core.BookChapter> currentChapters(Ref ref) {
final player = ref.watch(audiobookPlayerProvider); final session = ref.watch(sessionProvider);
player.slowPositionStreamInBook.listen((_) { if (session == null) {
ref.invalidateSelf(); return [];
}); }
final currentChapter = ref.watch(currentChapterProvider);
return player.currentChapter; if (currentChapter == null) {
return [];
}
final index = session.chapters.indexOf(currentChapter);
final total = session.chapters.length;
return session.chapters
.sublist(index - 3, (total - 3) <= (index + 17) ? total : index + 17);
} }
/// provides the book metadata of the currently playing book
@riverpod
BookMetadataExpanded? currentBookMetadata(Ref ref) {
final player = ref.watch(audiobookPlayerProvider);
if (player.book == null) return null;
return player.book!.metadata.asBookMetadataExpanded;
}
// /// volume of the player [0, 1]
// @riverpod
// double currentVolume(CurrentVolumeRef ref) {
// return 1;
// }

View file

@ -6,66 +6,39 @@ part of 'currently_playing_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$currentlyPlayingBookHash() => String _$currentChaptersHash() => r'f2cc6ec31b5a3a9471775b1c96b2bfc3a91f1c90';
r'f2c47028340d253be9440dc29f835328ff30c0e6';
/// See also [currentlyPlayingBook]. /// See also [currentChapters].
@ProviderFor(currentlyPlayingBook) @ProviderFor(currentChapters)
final currentlyPlayingBookProvider = final currentChaptersProvider =
AutoDisposeProvider<BookExpanded?>.internal( AutoDisposeProvider<List<core.BookChapter>>.internal(
currentlyPlayingBook, currentChapters,
name: r'currentlyPlayingBookProvider', name: r'currentChaptersProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$currentlyPlayingBookHash, : _$currentChaptersHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef<BookExpanded?>; typedef CurrentChaptersRef = AutoDisposeProviderRef<List<core.BookChapter>>;
String _$currentPlayingChapterHash() => String _$currentChapterHash() => r'f5f6d9e49cb7e455d032f7370f364d9ce30b8eb1';
r'4a64157089279c71279ccfdbcfc7b32543ecc88c';
/// provided the current chapter of the book being played /// See also [CurrentChapter].
/// @ProviderFor(CurrentChapter)
/// Copied from [currentPlayingChapter]. final currentChapterProvider =
@ProviderFor(currentPlayingChapter) AutoDisposeNotifierProvider<CurrentChapter, core.BookChapter?>.internal(
final currentPlayingChapterProvider = CurrentChapter.new,
AutoDisposeProvider<BookChapter?>.internal( name: r'currentChapterProvider',
currentPlayingChapter,
name: r'currentPlayingChapterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$currentPlayingChapterHash, : _$currentChapterHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead') typedef _$CurrentChapter = AutoDisposeNotifier<core.BookChapter?>;
// ignore: unused_element
typedef CurrentPlayingChapterRef = AutoDisposeProviderRef<BookChapter?>;
String _$currentBookMetadataHash() =>
r'f537ef4ef19280bc952de658ecf6520c535ae344';
/// provides the book metadata of the currently playing book
///
/// Copied from [currentBookMetadata].
@ProviderFor(currentBookMetadata)
final currentBookMetadataProvider =
AutoDisposeProvider<BookMetadataExpanded?>.internal(
currentBookMetadata,
name: r'currentBookMetadataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentBookMetadataHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef CurrentBookMetadataRef = AutoDisposeProviderRef<BookMetadataExpanded?>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,80 +0,0 @@
// this provider is used to manage the player form state
// it will inform about the percentage of the player expanded
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:miniplayer/miniplayer.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
part 'player_form.g.dart';
/// The height of the player when it is minimized
const double playerMinHeight = 70;
// const miniplayerPercentageDeclaration = 0.2;
extension on Ref {
// We can move the previous logic to a Ref extension.
// This enables reusing the logic between providers
T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
onDispose(notifier.dispose);
notifier.addListener(notifyListeners);
// We return the notifier to ease the usage a bit
return notifier;
}
}
@Riverpod(keepAlive: true)
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
Ref ref,
) {
final ValueNotifier<double> playerExpandProgress =
ValueNotifier(playerMinHeight);
return ref.disposeAndListenChangeNotifier(playerExpandProgress);
}
// @Riverpod(keepAlive: true)
// Raw<ValueNotifier<double>> dragDownPercentageNotifier(
// DragDownPercentageNotifierRef ref,
// ) {
// final ValueNotifier<double> notifier = ValueNotifier(0);
// return ref.disposeAndListenChangeNotifier(notifier);
// }
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
@Riverpod(keepAlive: true)
double playerHeight(
Ref ref,
) {
final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider);
// on change of the playerExpandProgress invalidate
playerExpandProgress.addListener(() {
ref.invalidateSelf();
});
// listen to the playerExpandProgressNotifier and return the value
return playerExpandProgress.value;
}
// final audioBookMiniplayerController = MiniplayerController();
@Riverpod(keepAlive: true)
bool isPlayerActive(
Ref ref,
) {
try {
final player = ref.watch(audiobookPlayerProvider);
if (player.book != null) {
return true;
} else {
final playerHeight = ref.watch(playerHeightProvider);
return playerHeight < playerMinHeight;
}
} catch (e) {
return false;
}
}

View file

@ -1,63 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_form.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$playerExpandProgressNotifierHash() =>
r'1ac7172d90a070f96222286edd1a176be197f378';
/// See also [playerExpandProgressNotifier].
@ProviderFor(playerExpandProgressNotifier)
final playerExpandProgressNotifierProvider =
Provider<Raw<ValueNotifier<double>>>.internal(
playerExpandProgressNotifier,
name: r'playerExpandProgressNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$playerExpandProgressNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PlayerExpandProgressNotifierRef
= ProviderRef<Raw<ValueNotifier<double>>>;
String _$playerHeightHash() => r'3f031eaffdffbb2c6ddf7eb1aba31bf1619260fc';
/// See also [playerHeight].
@ProviderFor(playerHeight)
final playerHeightProvider = Provider<double>.internal(
playerHeight,
name: r'playerHeightProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$playerHeightHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PlayerHeightRef = ProviderRef<double>;
String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98';
/// See also [isPlayerActive].
@ProviderFor(isPlayerActive)
final isPlayerActiveProvider = Provider<bool>.internal(
isPlayerActive,
name: r'isPlayerActiveProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isPlayerActiveHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef IsPlayerActiveRef = ProviderRef<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,188 +0,0 @@
import 'package:audio_service/audio_service.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as core;
import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart'
as core;
import 'package:vaani/features/player/core/audiobook_player_session.dart';
import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/globals.dart';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
import 'package:vaani/shared/utils/utils.dart';
part 'session_provider.g.dart';
@Riverpod(keepAlive: true)
Future<AbsAudioHandler> audioHandlerInit(Ref ref) async {
if (Utils.isWindows() || Utils.isLinux()) {
// JustAudioMediaKit.ensureInitialized(windows: false);
JustAudioMediaKit.ensureInitialized();
}
final audioService = await AudioService.init(
builder: () => AbsAudioHandler(ref),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.vaani.rang.channel.audio',
androidNotificationChannelName: 'ABSPlayback',
androidNotificationChannelDescription:
'Needed to control audio from lock screen',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
androidNotificationIcon: 'drawable/ic_stat_logo',
preloadArtwork: true,
),
);
return audioService;
}
@Riverpod(keepAlive: true)
class Player extends _$Player {
@override
AbsAudioHandler build() {
return ref.watch(audioHandlerInitProvider).requireValue;
}
}
@Riverpod(keepAlive: true)
class Session extends _$Session {
@override
core.PlaybackSessionExpanded? build() {
return null;
}
Future<void> load(String id, String? episodeId) async {
final audioService = ref.read(playerProvider);
await audioService.pause();
ref.read(playerStatusProvider.notifier).setLoading(id);
final api = ref.read(authenticatedApiProvider);
final playBack = await api.items.play(
libraryItemId: id,
parameters: core.PlayItemReqParams(
deviceInfo: core.DeviceInfoReqParams(
clientVersion: appVersion,
manufacturer: deviceManufacturer,
model: deviceModel,
sdkVersion: deviceSdkVersion,
clientName: appName,
deviceName: deviceName,
),
forceDirectPlay: false,
forceTranscode: false,
supportedMimeTypes: [
"audio/flac",
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/aac",
"audio/webm",
],
),
responseErrorHandler: _responseErrorHandler,
) as core.PlaybackSessionExpanded;
state = playBack;
final downloadManager = ref.read(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(playBack.libraryItemId).future);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
var bookPlayerSettings =
ref.read(bookSettingsProvider(playBack.libraryItemId)).playerSettings;
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
var configurePlayerForEveryBook =
appPlayerSettings.configurePlayerForEveryBook;
await Future.wait([
audioService.setSourceAudiobook(
playBack,
baseUrl: api.baseUrl,
token: api.token!,
downloadedUris: downloadedUris,
),
// set the volume
audioService.setVolume(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
),
// set the speed
audioService.setSpeed(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
),
]);
}
void _responseErrorHandler(http.Response response, [error]) {
if (response.statusCode != 200) {
appLogger.severe('Error with api: ${response.obfuscate()}, $error');
throw PlaybackSyncError(
'Error syncing position: ${response.body}, $error',
);
}
}
}
@Riverpod(keepAlive: true)
class CurrentChapter extends _$CurrentChapter {
@override
core.BookChapter? build() {
final player = ref.watch(playerProvider);
player.chapterStream.distinct().listen((chapter) {
update(chapter);
});
return player.currentChapter;
}
void update(core.BookChapter? chapter) {
if (state != chapter) {
state = chapter;
}
}
}
@Riverpod(keepAlive: true)
class PlaybackReporter extends _$PlaybackReporter {
@override
Future<core.PlaybackReporter?> build() async {
final session = ref.watch(sessionProvider);
if (session == null) {
return null;
}
final playerSettings = ref.watch(appSettingsProvider).playerSettings;
final player = ref.watch(playerProvider);
final api = ref.watch(authenticatedApiProvider);
final reporter = core.PlaybackReporter(
player,
api,
reportingInterval: playerSettings.playbackReportInterval,
markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft,
minimumPositionForReporting: playerSettings.minimumPositionForReporting,
session: session,
);
ref.onDispose(reporter.dispose);
return reporter;
}
}
class PlaybackSyncError implements Exception {
String message;
PlaybackSyncError([this.message = 'Error syncing playback']);
@override
String toString() {
return 'PlaybackSyncError: $message';
}
}

View file

@ -1,88 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'session_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audioHandlerInitHash() => r'c54f17757807f8bc14daff5095c34eb88ff2037b';
/// See also [audioHandlerInit].
@ProviderFor(audioHandlerInit)
final audioHandlerInitProvider = FutureProvider<AbsAudioHandler>.internal(
audioHandlerInit,
name: r'audioHandlerInitProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audioHandlerInitHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AudioHandlerInitRef = FutureProviderRef<AbsAudioHandler>;
String _$playerHash() => r'9599b094cdd9eca614c27ec5bdf2d5259d20ac5f';
/// See also [Player].
@ProviderFor(Player)
final playerProvider = NotifierProvider<Player, AbsAudioHandler>.internal(
Player.new,
name: r'playerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$playerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Player = Notifier<AbsAudioHandler>;
String _$sessionHash() => r'ce9405cc0b8247014924a005fa60fbc3bf92d5c6';
/// See also [Session].
@ProviderFor(Session)
final sessionProvider =
NotifierProvider<Session, core.PlaybackSessionExpanded?>.internal(
Session.new,
name: r'sessionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Session = Notifier<core.PlaybackSessionExpanded?>;
String _$currentChapterHash() => r'0be02fbc4f65b30da4ce18964ee457a18c4d1073';
/// See also [CurrentChapter].
@ProviderFor(CurrentChapter)
final currentChapterProvider =
NotifierProvider<CurrentChapter, core.BookChapter?>.internal(
CurrentChapter.new,
name: r'currentChapterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentChapterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CurrentChapter = Notifier<core.BookChapter?>;
String _$playbackReporterHash() => r'b9a2197a12e83f9760bd61ae2a1ad8dff82042e9';
/// See also [PlaybackReporter].
@ProviderFor(PlaybackReporter)
final playbackReporterProvider =
AsyncNotifierProvider<PlaybackReporter, core.PlaybackReporter?>.internal(
PlaybackReporter.new,
name: r'playbackReporterProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$playbackReporterHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PlaybackReporter = AsyncNotifier<core.PlaybackReporter?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/globals.dart' show playerMinHeight;
class MiniPlayerBottomPadding extends HookConsumerWidget { class MiniPlayerBottomPadding extends HookConsumerWidget {
const MiniPlayerBottomPadding({super.key}); const MiniPlayerBottomPadding({super.key});
@ -8,7 +9,7 @@ class MiniPlayerBottomPadding extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AnimatedSize( return AnimatedSize(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: ref.watch(isPlayerActiveProvider) child: ref.watch(playerStatusProvider).isPlaying()
? const SizedBox(height: playerMinHeight + 8) ? const SizedBox(height: playerMinHeight + 8)
: const SizedBox.shrink(), : const SizedBox.shrink(),
); );

View file

@ -1,15 +1,14 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/features/player/view/widgets/player_progress_bar.dart'; import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
import 'package:vaani/features/player/view/widgets/player_skip_chapter_start_end.dart'; import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/shared/widgets/not_implemented.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'widgets/audiobook_player_seek_button.dart'; import 'widgets/audiobook_player_seek_button.dart';
@ -40,172 +39,162 @@ class PlayerExpanded extends HookConsumerWidget {
final availWidth = MediaQuery.of(context).size.width; final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded // the image width when the player is expanded
final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9); final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
return Scaffold( return Column(
appBar: AppBar( children: [
leading: IconButton( // sized box for system status bar; not needed as not full screen
iconSize: 30, SizedBox(
icon: const Icon(Icons.keyboard_arrow_down), height: MediaQuery.of(context).padding.top,
onPressed: () => context.pop(),
), ),
actions: [
IconButton(
icon: const Icon(Icons.cast),
onPressed: () {
showNotImplementedToast(context);
},
),
],
),
body: Column(
children: [
// sized box for system status bar; not needed as not full screen
SizedBox(
height: MediaQuery.of(context).padding.top,
),
// the image // the image
Padding( Padding(
padding: EdgeInsets.only(top: AppElementSizes.paddingLarge), padding: EdgeInsets.only(top: AppElementSizes.paddingLarge),
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
// add a shadow to the image elevation hovering effect // add a shadow to the image elevation hovering effect
child: Container( child: PlayerExpandedImage(imageSize),
decoration: BoxDecoration( ),
boxShadow: [ ),
BoxShadow(
color: Theme.of(context) // the chapter title
.colorScheme Expanded(
.primary child: Padding(
.withValues(alpha: 0.1), padding: EdgeInsets.only(top: AppElementSizes.paddingRegular),
blurRadius: 32, child: currentChapter == null
spreadRadius: 8, ? const SizedBox()
), : Text(
], currentChapter.title,
), style: Theme.of(context).textTheme.titleLarge,
child: SizedBox( maxLines: 1,
height: imageSize, overflow: TextOverflow.ellipsis,
child: InkWell(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular,
),
child: BookCoverWidget(),
),
), ),
), ),
), ),
// the book name and author
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular),
child: Text(
[
session.displayTitle,
session.displayAuthor,
].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), ),
),
// the chapter title // the progress bar
Expanded( Expanded(
child: Padding( child: SizedBox(
padding: EdgeInsets.only(top: AppElementSizes.paddingRegular),
child: currentChapter == null
? const SizedBox()
: Text(
currentChapter.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// the book name and author
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular),
child: Text(
[
session.displayTitle,
session.displayAuthor,
].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// the progress bar
Expanded(
child: SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookChapterProgressBar(),
),
),
),
SizedBox(
width: imageSize, width: imageSize,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular, left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular, right: AppElementSizes.paddingRegular,
), ),
child: const AudiobookProgressBar(), child: const AudiobookChapterProgressBar(),
), ),
), ),
),
// the chapter skip buttons, seek 30 seconds back and forward, and play/pause button SizedBox(
Expanded( width: imageSize,
flex: 2, child: Padding(
child: SizedBox( padding: EdgeInsets.only(
width: imageSize, left: AppElementSizes.paddingRegular,
height: AppElementSizes.iconSizeRegular, right: AppElementSizes.paddingRegular,
child: Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: const AudiobookProgressBar(),
children: [ ),
// previous chapter ),
const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
const AudiobookPlayerSeekButton(isForward: false), Expanded(
AudiobookPlayerPlayPauseButton(), flex: 2,
// buttonSkipForwards child: SizedBox(
const AudiobookPlayerSeekButton(isForward: true), width: imageSize,
// next chapter height: AppElementSizes.iconSizeRegular,
const AudiobookPlayerSeekChapterButton(isForward: true), child: Row(
], mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
// previous chapter
const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton(),
// buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// next chapter
const AudiobookPlayerSeekChapterButton(isForward: true),
],
), ),
), ),
),
// speed control, sleep timer, chapter list, and settings // speed control, sleep timer, chapter list, and settings
Expanded( Expanded(
child: SizedBox( child: SizedBox(
width: imageSize, width: imageSize,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
// speed control // speed control
const PlayerSpeedAdjustButton(), const PlayerSpeedAdjustButton(),
const Spacer(), const Spacer(),
// sleep timer // sleep timer
const SleepTimerButton(), const SleepTimerButton(),
const Spacer(), const Spacer(),
// chapter list // chapter list
const ChapterSelectionButton(), const ChapterSelectionButton(),
const Spacer(), const Spacer(),
// //
SkipChapterStartEndButton(), SkipChapterStartEndButton(),
], ],
),
), ),
), ),
),
],
);
}
}
class PlayerExpandedImage extends StatelessWidget {
final double imageSize;
const PlayerExpandedImage(this.imageSize, {super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
blurRadius: 32,
spreadRadius: 8,
),
], ],
), ),
child: SizedBox(
height: imageSize,
child: InkWell(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppElementSizes.borderRadiusRegular,
),
child: BookCoverWidget(),
),
),
),
); );
} }
} }

View file

@ -0,0 +1,174 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/player_expanded.dart'
show PlayerExpandedImage;
import 'package:vaani/features/player/view/player_minimized.dart';
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart';
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart';
import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/globals.dart';
import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
var pendingPlayerModals = 0;
class PlayerExpandedDesktop extends HookConsumerWidget {
const PlayerExpandedDesktop({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(sessionProvider);
if (session == null) {
return SizedBox.shrink();
}
/// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer]
/// however, some properties need to start later than 0% and end before 100%
final currentChapter = ref.watch(currentChapterProvider);
// final currentBookMetadata = ref.watch(currentBookMetadataProvider);
// max height of the player is the height of the screen
final playerMaxHeight = MediaQuery.of(context).size.height;
final availWidth = MediaQuery.of(context).size.width;
// the image width when the player is expanded
final imageSize = min(playerMaxHeight * 0.5, availWidth * 0.9);
return Stack(
alignment: Alignment.bottomCenter,
children: [
Scaffold(
body: Padding(
padding: EdgeInsets.only(
top: AppElementSizes.paddingLarge,
bottom: playerMinHeight + 40,
),
child: Row(
children: [
Expanded(
flex: 45,
child: Column(
children: [
Align(
alignment: Alignment.topCenter,
// add a shadow to the image elevation hovering effect
child: PlayerExpandedImage(imageSize),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// previous chapter
const AudiobookPlayerSeekChapterButton(
isForward: false),
// buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton(),
// buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// next chapter
const AudiobookPlayerSeekChapterButton(
isForward: true),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// speed control
const PlayerSpeedAdjustButton(),
const Spacer(),
// sleep timer
const SleepTimerButton(),
const Spacer(),
//
SkipChapterStartEndButton(),
],
),
],
),
),
Expanded(
flex: 65,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
currentChapter == null
? SizedBox.shrink()
: Text(
currentChapter.title,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: ChapterSelection(),
),
],
),
),
],
),
),
),
Hero(tag: 'player_hero', child: const PlayerMinimized()),
],
);
}
}
class ChapterSelection extends HookConsumerWidget {
const ChapterSelection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentChapter = ref.watch(currentChapterProvider);
if (currentChapter == null) {
return SizedBox.shrink();
}
final currentChapters = ref.watch(currentChaptersProvider);
final currentChapterIndex = currentChapters.indexOf(currentChapter);
final theme = Theme.of(context);
return Scrollbar(
child: ListView.builder(
itemCount: currentChapters.length,
itemBuilder: (context, index) {
final chapter = currentChapters[index];
final isCurrent = currentChapterIndex == index;
final isPlayed = index < currentChapterIndex;
return ListTile(
autofocus: isCurrent,
iconColor: isPlayed && !isCurrent ? 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: () {
ref.read(playerProvider).skipToChapter(chapter.id);
},
);
},
),
);
}
}

View file

@ -4,7 +4,8 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
@ -28,7 +29,7 @@ class PlayerMinimized extends HookConsumerWidget {
// image // image
Padding( Padding(
padding: EdgeInsets.all(AppElementSizes.paddingSmall), padding: EdgeInsets.all(AppElementSizes.paddingSmall),
child: InkWell( child: GestureDetector(
onTap: () { onTap: () {
// navigate to item page // navigate to item page
context.pushNamed( context.pushNamed(
@ -114,7 +115,13 @@ class PlayerMinimizedFramework extends HookConsumerWidget {
final progress = final progress =
useStream(player.positionStreamInChapter, initialData: Duration.zero); useStream(player.positionStreamInChapter, initialData: Duration.zero);
return GestureDetector( return GestureDetector(
onTap: () => context.pushNamed(Routes.player.name), onTap: () {
if (GoRouterState.of(context).topRoute?.name != Routes.player.name) {
context.pushNamed(Routes.player.name);
} else {
context.pop();
}
},
child: Container( child: Container(
height: playerMinimizedHeight, height: playerMinimizedHeight,
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,

View file

@ -1,293 +0,0 @@
// import 'package:flutter/material.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:miniplayer/miniplayer.dart';
// import 'package:vaani/constants/sizes.dart';
// import 'package:vaani/features/player/providers/currently_playing_provider.dart';
// import 'package:vaani/features/player/providers/player_form.dart';
// import 'package:vaani/features/player/view/audiobook_player.dart';
// import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
// import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart';
// import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
// import 'package:vaani/shared/extensions/inverse_lerp.dart';
// import 'package:vaani/shared/widgets/not_implemented.dart';
// import 'widgets/audiobook_player_seek_button.dart';
// import 'widgets/audiobook_player_seek_chapter_button.dart';
// import 'widgets/chapter_selection_button.dart';
// import 'widgets/player_speed_adjust_button.dart';
// var pendingPlayerModals = 0;
// class PlayerWhenExpanded extends HookConsumerWidget {
// const PlayerWhenExpanded({
// super.key,
// required this.imageSize,
// required this.img,
// required this.percentageExpandedPlayer,
// required this.playPauseController,
// });
// /// padding values control the position of the image
// final double imageSize;
// final Widget img;
// final double percentageExpandedPlayer;
// final AnimationController playPauseController;
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// /// all the properties that help in building the widget are calculated from the [percentageExpandedPlayer]
// /// however, some properties need to start later than 0% and end before 100%
// const lateStart = 0.4;
// const earlyEnd = 1;
// final earlyPercentage = percentageExpandedPlayer
// .inverseLerp(
// lateStart,
// earlyEnd,
// )
// .clamp(0.0, 1.0);
// final currentChapter = ref.watch(currentPlayingChapterProvider);
// final currentBookMetadata = ref.watch(currentBookMetadataProvider);
// return Column(
// children: [
// // sized box for system status bar; not needed as not full screen
// 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,
// ),
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(top: 8.0 * earlyPercentage),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.max,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// // the down arrow
// IconButton(
// iconSize: 30,
// icon: const Icon(Icons.keyboard_arrow_down),
// onPressed: () {
// // minimize the player
// audioBookMiniplayerController.animateToHeight(
// state: PanelState.MIN,
// );
// },
// ),
// // the cast button
// IconButton(
// icon: const Icon(Icons.cast),
// onPressed: () {
// showNotImplementedToast(context);
// },
// ),
// ],
// ),
// ),
// ),
// ),
// // the image
// Padding(
// padding: EdgeInsets.only(
// top: AppElementSizes.paddingLarge * earlyPercentage,
// ),
// child: Align(
// alignment: Alignment.center,
// // add a shadow to the image elevation hovering effect
// child: Container(
// decoration: BoxDecoration(
// boxShadow: [
// BoxShadow(
// color: Theme.of(context)
// .colorScheme
// .primary
// .withValues(alpha: 0.1),
// blurRadius: 32 * earlyPercentage,
// spreadRadius: 8 * earlyPercentage,
// // offset: Offset(0, 16 * earlyPercentage),
// ),
// ],
// ),
// child: SizedBox(
// height: imageSize,
// child: InkWell(
// onTap: () {},
// child: ClipRRect(
// borderRadius: BorderRadius.circular(
// AppElementSizes.borderRadiusRegular * earlyPercentage,
// ),
// child: img,
// ),
// ),
// ),
// ),
// ),
// ),
// // the chapter title
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(
// top: AppElementSizes.paddingRegular * earlyPercentage,
// // horizontal: 16.0,
// ),
// // child: SizedBox(
// // same as the image width
// // width: imageSize,
// child: currentChapter == null
// ? const SizedBox()
// : Text(
// currentChapter.title,
// style: Theme.of(context).textTheme.titleLarge,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// // ),
// ),
// ),
// ),
// // the book name and author
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: Padding(
// padding: EdgeInsets.only(
// bottom: AppElementSizes.paddingRegular * earlyPercentage,
// // horizontal: 16.0,
// ),
// // child: SizedBox(
// // same as the image width
// // width: imageSize,
// child: Text(
// [
// currentBookMetadata?.title ?? '',
// currentBookMetadata?.authorName ?? '',
// ].join(' - '),
// style: Theme.of(context).textTheme.titleMedium?.copyWith(
// color: Theme.of(context)
// .colorScheme
// .onSurface
// .withValues(alpha: 0.7),
// ),
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// // ),
// ),
// ),
// ),
// // the progress bar
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// width: imageSize,
// child: Padding(
// padding: EdgeInsets.only(
// // top: AppElementSizes.paddingRegular * earlyPercentage,
// left: AppElementSizes.paddingRegular * earlyPercentage,
// right: AppElementSizes.paddingRegular * earlyPercentage,
// ),
// child: const AudiobookChapterProgressBar(),
// ),
// ),
// ),
// ),
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// width: imageSize,
// child: Padding(
// padding: EdgeInsets.only(
// // top: AppElementSizes.paddingRegular * earlyPercentage,
// left: AppElementSizes.paddingRegular * earlyPercentage,
// right: AppElementSizes.paddingRegular * earlyPercentage,
// ),
// child: const AudiobookProgressBar(),
// ),
// ),
// ),
// ),
// // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
// Expanded(
// flex: 2,
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// width: imageSize,
// height: AppElementSizes.iconSizeRegular,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// // previous chapter
// const AudiobookPlayerSeekChapterButton(isForward: false),
// // buttonSkipBackwards
// const AudiobookPlayerSeekButton(isForward: false),
// AudiobookPlayerPlayPauseButton(
// playPauseController: playPauseController,
// ),
// // buttonSkipForwards
// const AudiobookPlayerSeekButton(isForward: true),
// // next chapter
// const AudiobookPlayerSeekChapterButton(isForward: true),
// ],
// ),
// ),
// ),
// ),
// // speed control, sleep timer, chapter list, and settings
// Expanded(
// child: Opacity(
// opacity: earlyPercentage,
// child: SizedBox(
// // padding: EdgeInsets.only(
// // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
// // ),
// width: imageSize,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
// // speed control
// const PlayerSpeedAdjustButton(),
// const Spacer(),
// // sleep timer
// const SleepTimerButton(),
// const Spacer(),
// // chapter list
// const ChapterSelectionButton(),
// const Spacer(),
// //
// SkipChapterStartEndButton(),
// // settings
// // IconButton(
// // icon: const Icon(Icons.more_horiz),
// // onPressed: () {
// // // show toast
// // showNotImplementedToast(context);
// // },
// // ),
// ],
// ),
// ),
// ),
// ),
// ],
// );
// }
// }

View file

@ -1,155 +0,0 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_hooks/flutter_hooks.dart';
// import 'package:go_router/go_router.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:vaani/constants/sizes.dart';
// import 'package:vaani/features/player/providers/audiobook_player.dart';
// import 'package:vaani/features/player/providers/currently_playing_provider.dart';
// import 'package:vaani/features/player/view/audiobook_player.dart';
// import 'package:vaani/router/router.dart';
// class PlayerWhenMinimized extends HookConsumerWidget {
// const PlayerWhenMinimized({
// super.key,
// required this.availWidth,
// required this.maxImgSize,
// required this.imgWidget,
// required this.playPauseController,
// required this.percentageMiniplayer,
// });
// final double availWidth;
// final double maxImgSize;
// final Widget imgWidget;
// final AnimationController playPauseController;
// /// 0 - 1, from minimized to when switched to expanded player
// ///
// /// by the time 1 is reached only image should be visible in the center of the widget
// final double percentageMiniplayer;
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final player = ref.watch(audiobookPlayerProvider);
// final currentChapter = ref.watch(currentPlayingChapterProvider);
// final vanishingPercentage = 1 - percentageMiniplayer;
// // final progress =
// // useStream(player.slowPositionStreamInBook, initialData: Duration.zero);
// final progress =
// useStream(player.positionStream, initialData: Duration.zero);
// final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
// var barHeight = vanishingPercentage * 3;
// return Stack(
// alignment: Alignment.topCenter,
// children: [
// Row(
// children: [
// // image
// Padding(
// padding: EdgeInsets.only(
// left: ((availWidth - maxImgSize) / 2) * percentageMiniplayer,
// ),
// child: InkWell(
// onTap: () {
// // navigate to item page
// context.pushNamed(
// Routes.libraryItem.name,
// pathParameters: {
// Routes.libraryItem.pathParamName!:
// player.book!.libraryItemId,
// },
// );
// },
// child: ConstrainedBox(
// constraints: BoxConstraints(
// maxWidth: maxImgSize,
// ),
// child: imgWidget,
// ),
// ),
// ),
// // author and title of the book
// Expanded(
// child: Padding(
// padding: const EdgeInsets.only(left: 8),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// children: [
// // AutoScrollText(
// Text(
// '${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}',
// maxLines: 1, overflow: TextOverflow.ellipsis,
// // velocity:
// // const Velocity(pixelsPerSecond: Offset(16, 0)),
// style: Theme.of(context).textTheme.bodyLarge,
// ),
// Text(
// bookMetaExpanded?.authorName ?? '',
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// style: Theme.of(context).textTheme.bodyMedium!.copyWith(
// color: Theme.of(context)
// .colorScheme
// .onSurface
// .withValues(alpha: 0.7),
// ),
// ),
// ],
// ),
// ),
// ),
// // IconButton(
// // icon: const Icon(Icons.fullscreen),
// // onPressed: () {
// // controller.animateToHeight(state: PanelState.MAX);
// // },
// // ),
// // rewind button
// Opacity(
// opacity: vanishingPercentage,
// child: Padding(
// padding: const EdgeInsets.only(left: 8),
// child: IconButton(
// icon: const Icon(
// Icons.replay_30,
// size: AppElementSizes.iconSizeSmall,
// ),
// onPressed: () {},
// ),
// ),
// ),
// // play/pause button
// Opacity(
// opacity: vanishingPercentage,
// child: Padding(
// padding: const EdgeInsets.only(right: 8),
// child: AudiobookPlayerPlayPauseButton(
// playPauseController: playPauseController,
// ),
// ),
// ),
// ],
// ),
// SizedBox(
// height: barHeight,
// child: LinearProgressIndicator(
// // value: (progress.data ?? Duration.zero).inSeconds /
// // player.book!.duration.inSeconds,
// value: (progress.data ?? Duration.zero).inSeconds /
// (player.duration?.inSeconds ?? 1),
// color: Theme.of(context).colorScheme.onPrimaryContainer,
// backgroundColor: Theme.of(context).colorScheme.primaryContainer,
// ),
// ),
// ],
// );
// }
// }

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekButton extends HookConsumerWidget { class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({ const AudiobookPlayerSeekButton({

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({ const AudiobookPlayerSeekChapterButton({

View file

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/view/player_expanded.dart' import 'package:vaani/features/player/view/player_expanded.dart'
show pendingPlayerModals; show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';

View file

@ -3,7 +3,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/core/player_status.dart'; import 'package:vaani/features/player/core/player_status.dart';
import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({ const AudiobookPlayerPlayPauseButton({

View file

@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
class AudiobookChapterProgressBar extends HookConsumerWidget { class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({ const AudiobookChapterProgressBar({

View file

@ -5,7 +5,7 @@ import 'package:vaani/features/per_book_settings/providers/book_settings_provide
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/player/view/player_expanded.dart';
import 'package:vaani/features/player/view/widgets/speed_selector.dart'; import 'package:vaani/features/player/view/widgets/speed_selector.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
final _logger = Logger('PlayerSpeedAdjustButton'); final _logger = Logger('PlayerSpeedAdjustButton');
@ -16,13 +16,12 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(playerProvider);
final bookId = player.book?.libraryItemId ?? '_'; final bookId = player.session?.libraryItemId ?? '_';
final bookSettings = ref.watch(bookSettingsProvider(bookId)); final bookSettings = ref.watch(bookSettingsProvider(bookId));
final appSettings = ref.watch(appSettingsProvider); final appSettings = ref.watch(appSettingsProvider);
final notifier = ref.watch(audiobookPlayerProvider.notifier);
return TextButton( return TextButton(
child: Text('${player.speed}x'), child: Text('${player.player.speed}x'),
onPressed: () async { onPressed: () async {
pendingPlayerModals++; pendingPlayerModals++;
_logger.fine('opening speed selector'); _logger.fine('opening speed selector');
@ -32,7 +31,7 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget {
builder: (context) { builder: (context) {
return SpeedSelector( return SpeedSelector(
onSpeedSelected: (speed) { onSpeedSelected: (speed) {
notifier.setSpeed(speed); player.setSpeed(speed);
if (appSettings.playerSettings.configurePlayerForEveryBook) { if (appSettings.playerSettings.configurePlayerForEveryBook) {
ref ref
.read( .read(

View file

@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
const double itemExtent = 25; const double itemExtent = 25;
@ -22,7 +22,7 @@ class SpeedSelector extends HookConsumerWidget {
final appSettings = ref.watch(appSettingsProvider); final appSettings = ref.watch(appSettingsProvider);
final playerSettings = appSettings.playerSettings; final playerSettings = appSettings.playerSettings;
final speeds = playerSettings.speedOptions; final speeds = playerSettings.speedOptions;
final currentSpeed = ref.watch(audiobookPlayerProvider).speed; final currentSpeed = ref.watch(playerProvider).player.speed;
final speedState = useState(currentSpeed); final speedState = useState(currentSpeed);
// hook the onSpeedSelected function to the state // hook the onSpeedSelected function to the state

View file

@ -1,6 +1,6 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/playlist.dart'; import 'package:vaani/features/playlist/playlist.dart';
part 'playlist_provider.g.dart'; part 'playlist_provider.g.dart';

View file

@ -3,7 +3,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/settings/models/api_settings.dart' as model; import 'package:vaani/features/settings/models/api_settings.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_settings_provider.g.dart'; part 'api_settings_provider.g.dart';

View file

@ -3,7 +3,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/settings/models/app_settings.dart' as model; import 'package:vaani/features/settings/models/app_settings.dart' as model;
part 'app_settings_provider.g.dart'; part 'app_settings_provider.g.dart';

View file

@ -1,8 +1,8 @@
// a freezed class to store the settings of the app // a freezed class to store the settings of the app
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/features/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart'; import 'package:vaani/features/settings/models/authenticated_user.dart';
part 'api_settings.freezed.dart'; part 'api_settings.freezed.dart';
part 'api_settings.g.dart'; part 'api_settings.g.dart';

View file

@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/features/settings/models/audiobookshelf_server.dart';
part 'authenticated_user.freezed.dart'; part 'authenticated_user.freezed.dart';
part 'authenticated_user.g.dart'; part 'authenticated_user.g.dart';

View file

@ -8,11 +8,11 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart' as model; import 'package:vaani/features/settings/models/app_settings.dart' as model;
import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart'; import 'package:vaani/features/settings/view/widgets/navigation_with_switch_tile.dart';
class AppSettingsPage extends HookConsumerWidget { class AppSettingsPage extends HookConsumerWidget {
const AppSettingsPage({ const AppSettingsPage({

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/time_of_day.dart'; import 'package:vaani/shared/extensions/time_of_day.dart';
class AutoSleepTimerSettingsPage extends HookConsumerWidget { class AutoSleepTimerSettingsPage extends HookConsumerWidget {

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/simple_settings_page.dart' import 'package:vaani/features/settings/view/simple_settings_page.dart'
show SimpleSettingsPage; show SimpleSettingsPage;
class HomePageSettingsPage extends HookConsumerWidget { class HomePageSettingsPage extends HookConsumerWidget {

View file

@ -3,10 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/enum.dart'; import 'package:vaani/shared/extensions/enum.dart';
class NotificationSettingsPage extends HookConsumerWidget { class NotificationSettingsPage extends HookConsumerWidget {

View file

@ -4,9 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/duration_format.dart';
class PlayerSettingsPage extends HookConsumerWidget { class PlayerSettingsPage extends HookConsumerWidget {

View file

@ -3,10 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/enum.dart'; import 'package:vaani/shared/extensions/enum.dart';
class ShakeDetectorSettingsPage extends HookConsumerWidget { class ShakeDetectorSettingsPage extends HookConsumerWidget {

View file

@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/features/settings/view/simple_settings_page.dart';
class ThemeSettingsPage extends HookConsumerWidget { class ThemeSettingsPage extends HookConsumerWidget {
const ThemeSettingsPage({ const ThemeSettingsPage({

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:sensors_plus/sensors_plus.dart'; import 'package:sensors_plus/sensors_plus.dart';
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/features/settings/models/app_settings.dart';
final _logger = Logger('ShakeDetector'); final _logger = Logger('ShakeDetector');

View file

@ -2,17 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider; show sleepTimerProvider;
import 'package:vaani/settings/app_settings_provider.dart' import 'package:vaani/features/settings/app_settings_provider.dart'
show appSettingsProvider; show appSettingsProvider;
import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vibration/vibration.dart'; import 'package:vibration/vibration.dart';
import '../core/shake_detector.dart' as core; import 'shake_detector.dart' as core;
part 'shake_detector.g.dart'; part 'shake_detector_provider.g.dart';
Logger _logger = Logger('ShakeDetector'); Logger _logger = Logger('ShakeDetector');

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shake_detector.dart'; part of 'shake_detector_provider.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:vaani/features/player/core/audiobook_player_session.dart'; import 'package:vaani/features/player/core/audiobook_player.dart';
import 'package:vaani/shared/extensions/chapter.dart'; import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/utils/throttler.dart'; import 'package:vaani/shared/utils/throttler.dart';

View file

@ -1,7 +1,7 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/session_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core; import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core;
part 'skip_start_end_provider.g.dart'; part 'skip_start_end_provider.g.dart';

View file

@ -3,10 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/providers/session_provider.dart';
import 'package:vaani/features/player/view/player_expanded.dart'; import 'package:vaani/features/player/view/player_expanded.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/view/notification_settings_page.dart'; import 'package:vaani/features/settings/view/notification_settings_page.dart';
class SkipChapterStartEndButton extends HookConsumerWidget { class SkipChapterStartEndButton extends HookConsumerWidget {
const SkipChapterStartEndButton({super.key}); const SkipChapterStartEndButton({super.key});
@ -74,7 +73,6 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget {
bookSettings.copyWith bookSettings.copyWith
.playerSettings(skipChapterStart: interval), .playerSettings(skipChapterStart: interval),
); );
ref.read(audiobookPlayerProvider).setClip(start: interval);
}, },
), ),
), ),

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:vaani/features/player/core/audiobook_player.dart';
/// this timer pauses the music player after a certain duration /// this timer pauses the music player after a certain duration
/// ///
@ -33,7 +32,7 @@ class SleepTimer {
} }
/// The player to be paused /// The player to be paused
final AudiobookPlayer player; final AudioPlayer player;
/// The timer that will pause the player /// The timer that will pause the player
Timer? timer; Timer? timer;

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core; import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/time_of_day.dart'; import 'package:vaani/shared/extensions/time_of_day.dart';
part 'sleep_timer_provider.g.dart'; part 'sleep_timer_provider.g.dart';
@ -26,7 +26,7 @@ class SleepTimer extends _$SleepTimer {
var sleepTimer = core.SleepTimer( var sleepTimer = core.SleepTimer(
duration: sleepTimerSettings.defaultDuration, duration: sleepTimerSettings.defaultDuration,
player: ref.watch(simpleAudiobookPlayerProvider), player: ref.watch(playerProvider).player,
); );
ref.onDispose(sleepTimer.dispose); ref.onDispose(sleepTimer.dispose);
return sleepTimer; return sleepTimer;
@ -45,7 +45,7 @@ class SleepTimer extends _$SleepTimer {
} else { } else {
final timer = core.SleepTimer( final timer = core.SleepTimer(
duration: resultingDuration, duration: resultingDuration,
player: ref.watch(simpleAudiobookPlayerProvider), player: ref.watch(playerProvider).player,
); );
ref.onDispose(timer.dispose); ref.onDispose(timer.dispose);
state = timer; state = timer;

View file

@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$sleepTimerHash() => r'2679454a217d0630a833d730557ab4e4feac2e56'; String _$sleepTimerHash() => r'daaaf63d599fb991e71a0da0ca1075fb46ccc6be';
/// See also [SleepTimer]. /// See also [SleepTimer].
@ProviderFor(SleepTimer) @ProviderFor(SleepTimer)

View file

@ -8,7 +8,7 @@ import 'package:vaani/features/sleep_timer/core/sleep_timer.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider; show sleepTimerProvider;
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/duration_format.dart';
class SleepTimerButton extends HookConsumerWidget { class SleepTimerButton extends HookConsumerWidget {

View file

@ -14,9 +14,9 @@ import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/router/router.dart' show Routes; import 'package:vaani/router/router.dart' show Routes;
import 'package:vaani/settings/api_settings_provider.dart' import 'package:vaani/features/settings/api_settings_provider.dart'
show apiSettingsProvider; show apiSettingsProvider;
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/features/settings/models/models.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet; import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet;
import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer; import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer;

Some files were not shown because too many files have changed in this diff Show more