mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-17 14:59:35 +00:00
一堆乱七八糟的修改
播放页面增加桌面版
This commit is contained in:
parent
aee1fbde88
commit
3ba35b31b8
116 changed files with 1238 additions and 2592 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -10,6 +10,7 @@
|
||||||
"deeplinking",
|
"deeplinking",
|
||||||
"fullscreen",
|
"fullscreen",
|
||||||
"Lerp",
|
"Lerp",
|
||||||
|
"Librarys",
|
||||||
"miniplayer",
|
"miniplayer",
|
||||||
"mocktail",
|
"mocktail",
|
||||||
"nodename",
|
"nodename",
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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].
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
8
lib/db/cache/cache_key.dart
vendored
8
lib/db/cache/cache_key.dart
vendored
|
|
@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
174
lib/features/player/view/player_expanded_desktop.dart
Normal file
174
lib/features/player/view/player_expanded_desktop.dart
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
// // },
|
|
||||||
// // ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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({
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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({
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue