diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 3a165c1..31e0037 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -14,6 +14,7 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' isItemDownloadingProvider, itemDownloadProgressProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/generated/l10n.dart'; @@ -299,7 +300,8 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isBookPlaying = ref.watch(sessionProvider)?.libraryItemId == item.id; + final isBookPlaying = + ref.watch(currentBookProvider)?.libraryItemId == item.id; return IconButton( onPressed: () { @@ -431,12 +433,12 @@ class _LibraryItemPlayButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); + final currentBook = ref.watch(currentBookProvider); final book = item.media.asBookExpanded; final playerStatusNotifier = ref.watch(playerStatusProvider); final isLoading = playerStatusNotifier.isLoading(book.libraryItemId); final isCurrentBookSetInPlayer = - session?.libraryItemId == book.libraryItemId; + currentBook?.libraryItemId == book.libraryItemId; final isPlayingThisBook = playerStatusNotifier.isPlaying() && isCurrentBookSetInPlayer; @@ -466,9 +468,11 @@ class _LibraryItemPlayButton extends HookConsumerWidget { return ElevatedButton.icon( onPressed: () { - session?.libraryItemId == book.libraryItemId + currentBook?.libraryItemId == book.libraryItemId ? ref.read(playerProvider).togglePlayPause() - : ref.read(sessionProvider.notifier).load(book.libraryItemId, null); + : ref + .read(currentBookProvider.notifier) + .update(book, userMediaProgress?.currentTime); }, icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + book.libraryItemId, diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 68b641b..24da9c0 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -146,14 +146,13 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { } final mediaProgress = libraryItem.userMediaProgress; - if (mediaProgress == null && - player.session?.libraryItemId != libraryItem.id) { + if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { return const SizedBox.shrink(); } double progress; Duration remainingTime; - if (player.session?.libraryItemId == libraryItem.id) { + if (player.book?.libraryItemId == libraryItem.id) { // final positionStream = useStream(player.slowPositionStream); progress = (player.positionInBook).inSeconds / libraryItem.media.asBookExpanded.duration.inSeconds; diff --git a/lib/features/per_book_settings/models/book_settings.dart b/lib/features/per_book_settings/models/book_settings.dart index b4f0cda..6f59cc9 100644 --- a/lib/features/per_book_settings/models/book_settings.dart +++ b/lib/features/per_book_settings/models/book_settings.dart @@ -10,8 +10,20 @@ class BookSettings with _$BookSettings { const factory BookSettings({ required String bookId, @Default(NullablePlayerSettings()) NullablePlayerSettings playerSettings, + BookProgress? progress, }) = _BookSettings; factory BookSettings.fromJson(Map json) => _$BookSettingsFromJson(json); } + +@freezed +class BookProgress with _$BookProgress { + const factory BookProgress({ + required DateTime lastUpdate, + @Default(Duration.zero) Duration currentTime, + }) = _BookProgress; + + factory BookProgress.fromJson(Map json) => + _$BookProgressFromJson(json); +} diff --git a/lib/features/per_book_settings/models/book_settings.freezed.dart b/lib/features/per_book_settings/models/book_settings.freezed.dart index 63b55da..bc63d35 100644 --- a/lib/features/per_book_settings/models/book_settings.freezed.dart +++ b/lib/features/per_book_settings/models/book_settings.freezed.dart @@ -23,6 +23,7 @@ mixin _$BookSettings { String get bookId => throw _privateConstructorUsedError; NullablePlayerSettings get playerSettings => throw _privateConstructorUsedError; + BookProgress? get progress => throw _privateConstructorUsedError; /// Serializes this BookSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -40,9 +41,13 @@ abstract class $BookSettingsCopyWith<$Res> { BookSettings value, $Res Function(BookSettings) then) = _$BookSettingsCopyWithImpl<$Res, BookSettings>; @useResult - $Res call({String bookId, NullablePlayerSettings playerSettings}); + $Res call( + {String bookId, + NullablePlayerSettings playerSettings, + BookProgress? progress}); $NullablePlayerSettingsCopyWith<$Res> get playerSettings; + $BookProgressCopyWith<$Res>? get progress; } /// @nodoc @@ -62,6 +67,7 @@ class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings> $Res call({ Object? bookId = null, Object? playerSettings = null, + Object? progress = freezed, }) { return _then(_value.copyWith( bookId: null == bookId @@ -72,6 +78,10 @@ class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings> ? _value.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable as NullablePlayerSettings, + progress: freezed == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as BookProgress?, ) as $Val); } @@ -85,6 +95,20 @@ class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings> return _then(_value.copyWith(playerSettings: value) as $Val); }); } + + /// Create a copy of BookSettings + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $BookProgressCopyWith<$Res>? get progress { + if (_value.progress == null) { + return null; + } + + return $BookProgressCopyWith<$Res>(_value.progress!, (value) { + return _then(_value.copyWith(progress: value) as $Val); + }); + } } /// @nodoc @@ -95,10 +119,15 @@ abstract class _$$BookSettingsImplCopyWith<$Res> __$$BookSettingsImplCopyWithImpl<$Res>; @override @useResult - $Res call({String bookId, NullablePlayerSettings playerSettings}); + $Res call( + {String bookId, + NullablePlayerSettings playerSettings, + BookProgress? progress}); @override $NullablePlayerSettingsCopyWith<$Res> get playerSettings; + @override + $BookProgressCopyWith<$Res>? get progress; } /// @nodoc @@ -116,6 +145,7 @@ class __$$BookSettingsImplCopyWithImpl<$Res> $Res call({ Object? bookId = null, Object? playerSettings = null, + Object? progress = freezed, }) { return _then(_$BookSettingsImpl( bookId: null == bookId @@ -126,6 +156,10 @@ class __$$BookSettingsImplCopyWithImpl<$Res> ? _value.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable as NullablePlayerSettings, + progress: freezed == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as BookProgress?, )); } } @@ -135,7 +169,8 @@ class __$$BookSettingsImplCopyWithImpl<$Res> class _$BookSettingsImpl implements _BookSettings { const _$BookSettingsImpl( {required this.bookId, - this.playerSettings = const NullablePlayerSettings()}); + this.playerSettings = const NullablePlayerSettings(), + this.progress}); factory _$BookSettingsImpl.fromJson(Map json) => _$$BookSettingsImplFromJson(json); @@ -145,10 +180,12 @@ class _$BookSettingsImpl implements _BookSettings { @override @JsonKey() final NullablePlayerSettings playerSettings; + @override + final BookProgress? progress; @override String toString() { - return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)'; + return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings, progress: $progress)'; } @override @@ -158,12 +195,15 @@ class _$BookSettingsImpl implements _BookSettings { other is _$BookSettingsImpl && (identical(other.bookId, bookId) || other.bookId == bookId) && (identical(other.playerSettings, playerSettings) || - other.playerSettings == playerSettings)); + other.playerSettings == playerSettings) && + (identical(other.progress, progress) || + other.progress == progress)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, bookId, playerSettings); + int get hashCode => + Object.hash(runtimeType, bookId, playerSettings, progress); /// Create a copy of BookSettings /// with the given fields replaced by the non-null parameter values. @@ -184,7 +224,8 @@ class _$BookSettingsImpl implements _BookSettings { abstract class _BookSettings implements BookSettings { const factory _BookSettings( {required final String bookId, - final NullablePlayerSettings playerSettings}) = _$BookSettingsImpl; + final NullablePlayerSettings playerSettings, + final BookProgress? progress}) = _$BookSettingsImpl; factory _BookSettings.fromJson(Map json) = _$BookSettingsImpl.fromJson; @@ -193,6 +234,8 @@ abstract class _BookSettings implements BookSettings { String get bookId; @override NullablePlayerSettings get playerSettings; + @override + BookProgress? get progress; /// Create a copy of BookSettings /// with the given fields replaced by the non-null parameter values. @@ -201,3 +244,174 @@ abstract class _BookSettings implements BookSettings { _$$BookSettingsImplCopyWith<_$BookSettingsImpl> get copyWith => throw _privateConstructorUsedError; } + +BookProgress _$BookProgressFromJson(Map json) { + return _BookProgress.fromJson(json); +} + +/// @nodoc +mixin _$BookProgress { + DateTime get lastUpdate => throw _privateConstructorUsedError; + Duration get currentTime => throw _privateConstructorUsedError; + + /// Serializes this BookProgress to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BookProgress + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BookProgressCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BookProgressCopyWith<$Res> { + factory $BookProgressCopyWith( + BookProgress value, $Res Function(BookProgress) then) = + _$BookProgressCopyWithImpl<$Res, BookProgress>; + @useResult + $Res call({DateTime lastUpdate, Duration currentTime}); +} + +/// @nodoc +class _$BookProgressCopyWithImpl<$Res, $Val extends BookProgress> + implements $BookProgressCopyWith<$Res> { + _$BookProgressCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BookProgress + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? lastUpdate = null, + Object? currentTime = null, + }) { + return _then(_value.copyWith( + lastUpdate: null == lastUpdate + ? _value.lastUpdate + : lastUpdate // ignore: cast_nullable_to_non_nullable + as DateTime, + currentTime: null == currentTime + ? _value.currentTime + : currentTime // ignore: cast_nullable_to_non_nullable + as Duration, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BookProgressImplCopyWith<$Res> + implements $BookProgressCopyWith<$Res> { + factory _$$BookProgressImplCopyWith( + _$BookProgressImpl value, $Res Function(_$BookProgressImpl) then) = + __$$BookProgressImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime lastUpdate, Duration currentTime}); +} + +/// @nodoc +class __$$BookProgressImplCopyWithImpl<$Res> + extends _$BookProgressCopyWithImpl<$Res, _$BookProgressImpl> + implements _$$BookProgressImplCopyWith<$Res> { + __$$BookProgressImplCopyWithImpl( + _$BookProgressImpl _value, $Res Function(_$BookProgressImpl) _then) + : super(_value, _then); + + /// Create a copy of BookProgress + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? lastUpdate = null, + Object? currentTime = null, + }) { + return _then(_$BookProgressImpl( + lastUpdate: null == lastUpdate + ? _value.lastUpdate + : lastUpdate // ignore: cast_nullable_to_non_nullable + as DateTime, + currentTime: null == currentTime + ? _value.currentTime + : currentTime // ignore: cast_nullable_to_non_nullable + as Duration, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BookProgressImpl implements _BookProgress { + const _$BookProgressImpl( + {required this.lastUpdate, this.currentTime = Duration.zero}); + + factory _$BookProgressImpl.fromJson(Map json) => + _$$BookProgressImplFromJson(json); + + @override + final DateTime lastUpdate; + @override + @JsonKey() + final Duration currentTime; + + @override + String toString() { + return 'BookProgress(lastUpdate: $lastUpdate, currentTime: $currentTime)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BookProgressImpl && + (identical(other.lastUpdate, lastUpdate) || + other.lastUpdate == lastUpdate) && + (identical(other.currentTime, currentTime) || + other.currentTime == currentTime)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, lastUpdate, currentTime); + + /// Create a copy of BookProgress + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BookProgressImplCopyWith<_$BookProgressImpl> get copyWith => + __$$BookProgressImplCopyWithImpl<_$BookProgressImpl>(this, _$identity); + + @override + Map toJson() { + return _$$BookProgressImplToJson( + this, + ); + } +} + +abstract class _BookProgress implements BookProgress { + const factory _BookProgress( + {required final DateTime lastUpdate, + final Duration currentTime}) = _$BookProgressImpl; + + factory _BookProgress.fromJson(Map json) = + _$BookProgressImpl.fromJson; + + @override + DateTime get lastUpdate; + @override + Duration get currentTime; + + /// Create a copy of BookProgress + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BookProgressImplCopyWith<_$BookProgressImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/per_book_settings/models/book_settings.g.dart b/lib/features/per_book_settings/models/book_settings.g.dart index a66fba1..8711ad8 100644 --- a/lib/features/per_book_settings/models/book_settings.g.dart +++ b/lib/features/per_book_settings/models/book_settings.g.dart @@ -13,10 +13,28 @@ _$BookSettingsImpl _$$BookSettingsImplFromJson(Map json) => ? const NullablePlayerSettings() : NullablePlayerSettings.fromJson( json['playerSettings'] as Map), + progress: json['progress'] == null + ? null + : BookProgress.fromJson(json['progress'] as Map), ); Map _$$BookSettingsImplToJson(_$BookSettingsImpl instance) => { 'bookId': instance.bookId, 'playerSettings': instance.playerSettings, + 'progress': instance.progress, + }; + +_$BookProgressImpl _$$BookProgressImplFromJson(Map json) => + _$BookProgressImpl( + lastUpdate: DateTime.parse(json['lastUpdate'] as String), + currentTime: json['currentTime'] == null + ? Duration.zero + : Duration(microseconds: (json['currentTime'] as num).toInt()), + ); + +Map _$$BookProgressImplToJson(_$BookProgressImpl instance) => + { + 'lastUpdate': instance.lastUpdate.toIso8601String(), + 'currentTime': instance.currentTime.inMicroseconds, }; diff --git a/lib/features/per_book_settings/providers/book_settings_provider.dart b/lib/features/per_book_settings/providers/book_settings_provider.dart index 10be5e2..c9b7312 100644 --- a/lib/features/per_book_settings/providers/book_settings_provider.dart +++ b/lib/features/per_book_settings/providers/book_settings_provider.dart @@ -19,9 +19,9 @@ model.BookSettings readFromBoxOrCreate(String bookId) { } else { // create a new settings object final settings = model.BookSettings( - bookId: bookId, - playerSettings: const NullablePlayerSettings(), - ); + bookId: bookId, + playerSettings: const NullablePlayerSettings(), + progress: model.BookProgress(lastUpdate: DateTime.now())); _logger.fine('created new book settings for $bookId: $settings'); writeToBox(settings); return settings; @@ -56,3 +56,18 @@ class BookSettings extends _$BookSettings { updateState(newSettings, force: force); } } + +@riverpod +class BookProgressSettings extends _$BookProgressSettings { + @override + model.BookProgress build(String bookId) { + final progress = + ref.read(bookSettingsProvider(bookId).select((v) => v.progress)); + if (progress == null) { + return model.BookProgress( + lastUpdate: DateTime.now(), + ); + } + return progress; + } +} diff --git a/lib/features/per_book_settings/providers/book_settings_provider.g.dart b/lib/features/per_book_settings/providers/book_settings_provider.g.dart index a620d34..3a37e18 100644 --- a/lib/features/per_book_settings/providers/book_settings_provider.g.dart +++ b/lib/features/per_book_settings/providers/book_settings_provider.g.dart @@ -172,5 +172,153 @@ class _BookSettingsProviderElement @override String get bookId => (origin as BookSettingsProvider).bookId; } + +String _$bookProgressSettingsHash() => + r'be890f6b4f90565620a48c347cb86266fc232374'; + +abstract class _$BookProgressSettings + extends BuildlessAutoDisposeNotifier { + late final String bookId; + + model.BookProgress build( + String bookId, + ); +} + +/// See also [BookProgressSettings]. +@ProviderFor(BookProgressSettings) +const bookProgressSettingsProvider = BookProgressSettingsFamily(); + +/// See also [BookProgressSettings]. +class BookProgressSettingsFamily extends Family { + /// See also [BookProgressSettings]. + const BookProgressSettingsFamily(); + + /// See also [BookProgressSettings]. + BookProgressSettingsProvider call( + String bookId, + ) { + return BookProgressSettingsProvider( + bookId, + ); + } + + @override + BookProgressSettingsProvider getProviderOverride( + covariant BookProgressSettingsProvider provider, + ) { + return call( + provider.bookId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'bookProgressSettingsProvider'; +} + +/// See also [BookProgressSettings]. +class BookProgressSettingsProvider extends AutoDisposeNotifierProviderImpl< + BookProgressSettings, model.BookProgress> { + /// See also [BookProgressSettings]. + BookProgressSettingsProvider( + String bookId, + ) : this._internal( + () => BookProgressSettings()..bookId = bookId, + from: bookProgressSettingsProvider, + name: r'bookProgressSettingsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$bookProgressSettingsHash, + dependencies: BookProgressSettingsFamily._dependencies, + allTransitiveDependencies: + BookProgressSettingsFamily._allTransitiveDependencies, + bookId: bookId, + ); + + BookProgressSettingsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.bookId, + }) : super.internal(); + + final String bookId; + + @override + model.BookProgress runNotifierBuild( + covariant BookProgressSettings notifier, + ) { + return notifier.build( + bookId, + ); + } + + @override + Override overrideWith(BookProgressSettings Function() create) { + return ProviderOverride( + origin: this, + override: BookProgressSettingsProvider._internal( + () => create()..bookId = bookId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + bookId: bookId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _BookProgressSettingsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BookProgressSettingsProvider && other.bookId == bookId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, bookId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin BookProgressSettingsRef + on AutoDisposeNotifierProviderRef { + /// The parameter `bookId` of this provider. + String get bookId; +} + +class _BookProgressSettingsProviderElement + extends AutoDisposeNotifierProviderElement with BookProgressSettingsRef { + _BookProgressSettingsProviderElement(super.provider); + + @override + String get bookId => (origin as BookProgressSettingsProvider).bookId; +} // 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 diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart index b2be336..921e085 100644 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -14,7 +14,7 @@ final _logger = Logger('PlaybackReporter'); /// and also report when the player is paused/stopped/finished/playing class PlaybackReporter { /// The player to watch - final AudiobookPlayer player; + final AbsAudioHandler player; /// the api to report to final AudiobookshelfApi authenticatedApi; @@ -75,7 +75,7 @@ class PlaybackReporter { this.markCompleteWhenTimeLeft = const Duration(seconds: 5), }) : _reportingInterval = reportingInterval { // initial conditions - if (player.playing) { + if (player.player.playing) { _stopwatch.start(); _setReportTimerIfNotAlready(); _logger.fine('starting stopwatch'); diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.dart index bbeecfa..8688b2b 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.dart @@ -1,20 +1,17 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart' +import 'package:vaani/features/playback_reporting/core/playback_reporter.dart' as core; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/globals.dart'; part 'playback_reporter_provider.g.dart'; -@riverpod +@Riverpod(keepAlive: true) class PlaybackReporter extends _$PlaybackReporter { @override - Future build() async { - final session = ref.watch(sessionProvider); - if (session == null) { - return null; - } + Future build() async { final playerSettings = ref.watch(appSettingsProvider).playerSettings; final player = ref.watch(playerProvider); final api = ref.watch(authenticatedApiProvider); @@ -25,7 +22,12 @@ class PlaybackReporter extends _$PlaybackReporter { reportingInterval: playerSettings.playbackReportInterval, markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, minimumPositionForReporting: playerSettings.minimumPositionForReporting, - session: session, + deviceName: deviceName, + deviceModel: deviceModel, + deviceSdkVersion: deviceSdkVersion, + deviceClientName: appName, + deviceClientVersion: appVersion, + deviceManufacturer: deviceManufacturer, ); ref.onDispose(reporter.dispose); return reporter; diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart index 6abe6c2..320a962 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -6,12 +6,12 @@ part of 'playback_reporter_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$playbackReporterHash() => r'f9be5d6e4b07815ec669406cede4b00d2278e3af'; +String _$playbackReporterHash() => r'23b9770d279d921a5766fc2dda20f76dd3e181ed'; /// See also [PlaybackReporter]. @ProviderFor(PlaybackReporter) -final playbackReporterProvider = AutoDisposeAsyncNotifierProvider< - PlaybackReporter, core.PlaybackReporter?>.internal( +final playbackReporterProvider = + AsyncNotifierProvider.internal( PlaybackReporter.new, name: r'playbackReporterProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -21,6 +21,6 @@ final playbackReporterProvider = AutoDisposeAsyncNotifierProvider< allTransitiveDependencies: null, ); -typedef _$PlaybackReporter = AutoDisposeAsyncNotifier; +typedef _$PlaybackReporter = AsyncNotifier; // 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 diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 37c2d8f..f9cb632 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -6,21 +6,28 @@ 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:logging/logging.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/features/settings/app_settings_provider.dart'; +import 'package:vaani/features/settings/models/app_settings.dart'; import 'package:vaani/shared/extensions/chapter.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; // add a small offset so the display does not show the previous chapter for a split second final offset = Duration(milliseconds: 10); +final _logger = Logger('AudiobookPlayer'); + class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final AudioPlayer _player = AudioPlayer(); // final List _playlist = []; final Ref ref; - PlaybackSessionExpanded? _session; + BookExpanded? _book; + BookExpanded? get book => _book; final _currentChapterObject = BehaviorSubject.seeded(null); AbsAudioHandler(this.ref) { @@ -40,7 +47,7 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { } }); _player.positionStream.distinct().listen((position) { - final chapter = _session?.findChapterAtTime(positionInBook); + final chapter = _book?.findChapterAtTime(positionInBook); if (chapter != currentChapter) { _currentChapterObject.sink.add(chapter); } @@ -49,46 +56,68 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // 加载有声书 Future setSourceAudiobook( - PlaybackSessionExpanded playbackSession, { + BookExpanded book, { + bool preload = true, required Uri baseUrl, required String token, + Duration? initialPosition, List? downloadedUris, }) async { - _session = playbackSession; + 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; + } + _book = book; // 添加所有音轨 - List audioSources = []; - for (final track in playbackSession.audioTracks) { - audioSources.add( - AudioSource.uri( - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), - ), - ); - } + List audioSources = book.tracks + .map( + (track) => AudioSource.uri( + _getUri(track, downloadedUris, baseUrl: baseUrl, token: token), + ), + ) + .toList(); + final title = appSettings.notificationSettings.primaryTitle + .formatNotificationTitle(book); + final album = appSettings.notificationSettings.secondaryTitle + .formatNotificationTitle(book); 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, + id: book.libraryItemId, + title: title, + album: album, + displayTitle: title, + displaySubtitle: album, + duration: book.duration, artUri: Uri.parse( - '$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token', + '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token', ), ), ); - final track = playbackSession.findTrackAtTime(playbackSession.currentTime); - final index = playbackSession.audioTracks.indexOf(track); - - await _player.setAudioSources( + final trackToPlay = book.findTrackAtTime(initialPosition ?? Duration.zero); + final initialIndex = book.tracks.indexOf(trackToPlay); + final initialPositionInTrack = initialPosition != null + ? initialPosition - trackToPlay.startOffset + : null; + await _player + .setAudioSources( audioSources, - initialIndex: index, - initialPosition: playbackSession.currentTime - track.startOffset, - ); - _player.seek(playbackSession.currentTime - track.startOffset, index: index); + preload: preload, + initialIndex: initialIndex, + initialPosition: initialPositionInTrack, + ) + .catchError((error) { + _logger.shout('Error in setting audio source: $error'); + return null; + }); + // _player.seek(initialPositionInTrack, index: initialIndex); await play(); // 恢复上次播放位置(如果有) // if (initialPosition != null) { @@ -106,23 +135,23 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // 核心功能:跳转到指定章节 Future skipToChapter(int chapterId) async { - if (_session == null) return; + if (_book == null) return; - final chapter = _session!.chapters.firstWhere( + final chapter = _book!.chapters.firstWhere( (ch) => ch.id == chapterId, orElse: () => throw Exception('Chapter not found'), ); await seekInBook(chapter.start + offset); } - PlaybackSessionExpanded? get session => _session; + BookExpanded? get Book => _book; // 当前音轨 AudioTrack? get currentTrack { - if (_session == null || _player.currentIndex == null) { + if (_book == null || _player.currentIndex == null) { return null; } - return _session!.audioTracks[_player.currentIndex!]; + return _book!.tracks[_player.currentIndex!]; } // 当前章节 @@ -187,10 +216,12 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { Future togglePlayPause() async { // check if book is set - if (_session == null) { - return Future.value(); + if (_book == null) { + _logger.warning('No book is set, not toggling play/pause'); } - _player.playerState.playing ? await pause() : await play(); + return switch (_player.playerState) { + PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(), + }; } // 播放控制方法 @@ -207,7 +238,7 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // 重写上一曲/下一曲为章节导航 @override Future skipToNext() async { - if (_session == null) { + if (_book == null) { // 回退到默认行为 return _player.seekToNext(); } @@ -216,10 +247,10 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // 回退到默认行为 return _player.seekToNext(); } - final chapterIndex = _session!.chapters.indexOf(chapter); - if (chapterIndex < _session!.chapters.length - 1) { + final chapterIndex = _book!.chapters.indexOf(chapter); + if (chapterIndex < _book!.chapters.length - 1) { // 跳到下一章 - final nextChapter = _session!.chapters[chapterIndex + 1]; + final nextChapter = _book!.chapters[chapterIndex + 1]; await skipToChapter(nextChapter.id); } } @@ -227,13 +258,13 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { @override Future skipToPrevious() async { final chapter = currentChapter; - if (_session == null || chapter == null) { + if (_book == null || chapter == null) { return _player.seekToPrevious(); } - final currentIndex = _session!.chapters.indexOf(chapter); + final currentIndex = _book!.chapters.indexOf(chapter); if (currentIndex > 0) { // 跳到上一章 - final prevChapter = _session!.chapters[currentIndex - 1]; + final prevChapter = _book!.chapters[currentIndex - 1]; await skipToChapter(prevChapter.id); } else { // 已经是第一章,回到开头 @@ -264,10 +295,10 @@ class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // 核心功能:跳转到全局时间位置 Future seekInBook(Duration globalPosition) async { - if (_session == null) return; + if (_book == null) return; // 找到目标音轨和在音轨内的位置 - final track = _session!.findTrackAtTime(globalPosition); - final index = _session!.audioTracks.indexOf(track); + final track = _book!.findTrackAtTime(globalPosition); + final index = _book!.tracks.indexOf(track); Duration positionInTrack = globalPosition - track.startOffset; if (positionInTrack < Duration.zero) { positionInTrack = Duration.zero; @@ -332,7 +363,46 @@ Uri _getUri( Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); } -extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded { +extension FormatNotificationTitle on String { + String formatNotificationTitle(BookExpanded book) { + return replaceAllMapped( + RegExp(r'\$(\w+)'), + (match) { + final type = match.group(1); + return NotificationTitleType.values + .firstWhere((element) => element.name == type) + .extractFrom(book) ?? + match.group(0) ?? + ''; + }, + ); + } +} + +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) { return chapters.firstWhere( (element) { @@ -343,23 +413,23 @@ extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded { } AudioTrack findTrackAtTime(Duration position) { - return audioTracks.firstWhere( + return tracks.firstWhere( (element) { return element.startOffset <= position && element.startOffset + element.duration >= position + offset; }, - orElse: () => audioTracks.first, + orElse: () => tracks.first, ); } int findTrackIndexAtTime(Duration position) { - return audioTracks.indexWhere((element) { + return tracks.indexWhere((element) { return element.startOffset <= position && element.startOffset + element.duration >= position + offset; }); } Duration getTrackStartOffset(int index) { - return audioTracks[index].startOffset; + return tracks[index].startOffset; } } diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 955b9cd..7d7651d 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -2,15 +2,7 @@ import 'package:audio_service/audio_service.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: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/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'; @@ -46,59 +38,59 @@ class Player extends _$Player { } } -@Riverpod(keepAlive: true) -class Session extends _$Session { - @override - core.PlaybackSessionExpanded? build() { - return null; - } +// @Riverpod(keepAlive: true) +// class Session extends _$Session { +// @override +// core.PlaybackSessionExpanded? build() { +// return null; +// } - Future 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 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 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 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); - var bookPlayerSettings = - ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; - var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; +// var bookPlayerSettings = +// ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; +// var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; +// 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, - ), - ]); - } -} +// 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, +// ), +// ]); +// } +// } class PlaybackSyncError implements Exception { String message; diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index ee8520f..3fe6263 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -37,20 +37,5 @@ final playerProvider = NotifierProvider.internal( ); typedef _$Player = Notifier; -String _$sessionHash() => r'c171809249c3021dc445dc1ba90fe8626a3d3b54'; - -/// See also [Session]. -@ProviderFor(Session) -final sessionProvider = - NotifierProvider.internal( - Session.new, - name: r'sessionProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$sessionHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$Session = Notifier; // 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 diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 0fc0c3f..bdd0fc8 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,10 +1,74 @@ import 'package:hooks_riverpod/hooks_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/player/providers/audiobook_player.dart'; +import 'package:vaani/features/settings/app_settings_provider.dart'; +import 'package:vaani/globals.dart'; part 'currently_playing_provider.g.dart'; +@riverpod +class CurrentBook extends _$CurrentBook { + @override + core.BookExpanded? build() { + return null; + } + + Future update(core.BookExpanded book, Duration? currentTime) async { + final audioService = ref.read(playerProvider); + if (state == book) { + appLogger.info('Book was already set'); + if (audioService.player.playing) { + appLogger.info('Pausing the book'); + await audioService.pause(); + return; + } else { + await audioService.play(); + } + } + state = book; + final api = ref.read(authenticatedApiProvider); + final downloadManager = ref.read(simpleDownloadManagerProvider); + final libItem = + await ref.read(libraryItemProvider(state!.libraryItemId).future); + final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); + + var bookPlayerSettings = + ref.read(bookSettingsProvider(state!.libraryItemId)).playerSettings; + var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; + + var configurePlayerForEveryBook = + appPlayerSettings.configurePlayerForEveryBook; + await Future.wait([ + audioService.setSourceAudiobook( + state!, + baseUrl: api.baseUrl, + token: api.token!, + initialPosition: currentTime, + 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 CurrentChapter extends _$CurrentChapter { @override @@ -25,16 +89,16 @@ class CurrentChapter extends _$CurrentChapter { @riverpod List currentChapters(Ref ref) { - final session = ref.watch(sessionProvider); - if (session == null) { + final book = ref.watch(currentBookProvider); + if (book == null) { return []; } final currentChapter = ref.watch(currentChapterProvider); if (currentChapter == null) { return []; } - final index = session.chapters.indexOf(currentChapter); - final total = session.chapters.length; - return session.chapters + final index = book.chapters.indexOf(currentChapter); + final total = book.chapters.length; + return book.chapters .sublist(index - 3, (total - 3) <= (index + 17) ? total : index + 17); } diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index 7678af0..b353d18 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -6,7 +6,7 @@ part of 'currently_playing_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$currentChaptersHash() => r'f2cc6ec31b5a3a9471775b1c96b2bfc3a91f1c90'; +String _$currentChaptersHash() => r'a25733d8085a2ce7dbc16fa2bf14f00ab8e2a623'; /// See also [currentChapters]. @ProviderFor(currentChapters) @@ -24,6 +24,21 @@ final currentChaptersProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef CurrentChaptersRef = AutoDisposeProviderRef>; +String _$currentBookHash() => r'8dd534821b2b02a0259c6e6bde58012b880225c5'; + +/// See also [CurrentBook]. +@ProviderFor(CurrentBook) +final currentBookProvider = + AutoDisposeNotifierProvider.internal( + CurrentBook.new, + name: r'currentBookProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$currentBookHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$CurrentBook = AutoDisposeNotifier; String _$currentChapterHash() => r'f5f6d9e49cb7e455d032f7370f364d9ce30b8eb1'; /// See also [CurrentChapter]. diff --git a/lib/features/player/view/player_expanded.dart b/lib/features/player/view/player_expanded.dart index 6067529..5cceffc 100644 --- a/lib/features/player/view/player_expanded.dart +++ b/lib/features/player/view/player_expanded.dart @@ -3,12 +3,12 @@ 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/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_progress_bar.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/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; import 'widgets/audiobook_player_seek_button.dart'; @@ -25,8 +25,8 @@ class PlayerExpanded extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); - if (session == null) { + final currentBook = ref.watch(currentBookProvider); + if (currentBook == null) { return SizedBox.shrink(); } @@ -77,8 +77,8 @@ class PlayerExpanded extends HookConsumerWidget { padding: EdgeInsets.only(bottom: AppElementSizes.paddingRegular), child: Text( [ - session.displayTitle, - session.displayAuthor, + currentBook.metadata.title ?? '', + currentBook.metadata.asBookMetadataExpanded.authorName ?? '', ].join(' - '), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) diff --git a/lib/features/player/view/player_expanded_desktop.dart b/lib/features/player/view/player_expanded_desktop.dart index 06bf6e7..f0dafda 100644 --- a/lib/features/player/view/player_expanded_desktop.dart +++ b/lib/features/player/view/player_expanded_desktop.dart @@ -27,8 +27,8 @@ class PlayerExpandedDesktop extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); - if (session == null) { + final book = ref.watch(currentBookProvider); + if (book == null) { return SizedBox.shrink(); } @@ -49,6 +49,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget { body: Padding( padding: EdgeInsets.only( top: AppElementSizes.paddingLarge, + right: AppElementSizes.paddingRegular, bottom: playerMinHeight + 40, ), child: Row( @@ -108,7 +109,18 @@ class PlayerExpandedDesktop extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Expanded( - child: ChapterSelection(), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + right: BorderSide( + color: Theme.of(context).focusColor, + width: 1.0, + style: BorderStyle.solid, // 可以设置为 dashed 虚线 + ), + ), + ), + child: ChapterSelection(), + ), ), ], ), diff --git a/lib/features/player/view/player_minimized.dart b/lib/features/player/view/player_minimized.dart index 63f8d8c..eae9cd5 100644 --- a/lib/features/player/view/player_minimized.dart +++ b/lib/features/player/view/player_minimized.dart @@ -8,6 +8,7 @@ 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/router/router.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; /// The height of the player when it is minimized @@ -18,8 +19,8 @@ class PlayerMinimized extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); - if (session == null) { + final currentBook = ref.watch(currentBookProvider); + if (currentBook == null) { return SizedBox.shrink(); } final currentChapter = ref.watch(currentChapterProvider); @@ -35,7 +36,7 @@ class PlayerMinimized extends HookConsumerWidget { context.pushNamed( Routes.libraryItem.name, pathParameters: { - Routes.libraryItem.pathParamName!: session.libraryItemId, + Routes.libraryItem.pathParamName!: currentBook.libraryItemId, }, ); }, @@ -60,14 +61,14 @@ class PlayerMinimized extends HookConsumerWidget { children: [ // AutoScrollText( PlatformText( - '${session.displayTitle} - ${currentChapter?.title ?? ''}', + '${currentBook.metadata.title ?? ''} - ${currentChapter?.title ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), style: Theme.of(context).textTheme.bodyLarge, ), PlatformText( - session.displayAuthor, + currentBook.metadata.asBookMetadataExpanded.authorName ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium!.copyWith( diff --git a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart index dd1fa72..6a84d2d 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart @@ -21,13 +21,12 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { size: AppElementSizes.iconSizeSmall, ), onPressed: () { - if (player.session == null) { + if (player.book == null) { return; } // if chapter does not exist, go to the start or end of the book if (player.currentChapter == null) { - player - .seekInBook(isForward ? player.session!.duration : Duration.zero); + player.seekInBook(isForward ? player.book!.duration : Duration.zero); return; } if (isForward) { diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 8b31ea5..d8b6a12 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -54,7 +54,7 @@ class ChapterSelectionModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); + final session = ref.watch(currentBookProvider); final currentChapter = ref.watch(currentChapterProvider); final currentChapterIndex = currentChapter?.id; diff --git a/lib/features/player/view/widgets/player_progress_bar.dart b/lib/features/player/view/widgets/player_progress_bar.dart index 0981e72..9675732 100644 --- a/lib/features/player/view/widgets/player_progress_bar.dart +++ b/lib/features/player/view/widgets/player_progress_bar.dart @@ -38,7 +38,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { progress: currentChapterProgress ?? position.data ?? const Duration(seconds: 0), total: currentChapter == null - ? player.session?.duration ?? const Duration(seconds: 0) + ? player.book?.duration ?? const Duration(seconds: 0) : currentChapter.end - currentChapter.start, // ! TODO add onSeek onSeek: (duration) { @@ -74,7 +74,7 @@ class AudiobookProgressBar extends HookConsumerWidget { height: AppElementSizes.barHeightLarge, child: LinearProgressIndicator( value: (position.data ?? const Duration(seconds: 0)).inSeconds / - (player.session?.duration ?? const Duration(seconds: 0)).inSeconds, + (player.book?.duration ?? const Duration(seconds: 0)).inSeconds, borderRadius: BorderRadiusGeometry.all(Radius.circular(10)), ), ); diff --git a/lib/features/player/view/widgets/player_speed_adjust_button.dart b/lib/features/player/view/widgets/player_speed_adjust_button.dart index 66274f9..43925a6 100644 --- a/lib/features/player/view/widgets/player_speed_adjust_button.dart +++ b/lib/features/player/view/widgets/player_speed_adjust_button.dart @@ -17,7 +17,7 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(playerProvider); - final bookId = player.session?.libraryItemId ?? '_'; + final bookId = player.book?.libraryItemId ?? '_'; final bookSettings = ref.watch(bookSettingsProvider(bookId)); final appSettings = ref.watch(appSettingsProvider); return TextButton( diff --git a/lib/features/shake_detector/shake_detector_provider.dart b/lib/features/shake_detector/shake_detector_provider.dart index 8a84c65..ab16902 100644 --- a/lib/features/shake_detector/shake_detector_provider.dart +++ b/lib/features/shake_detector/shake_detector_provider.dart @@ -45,7 +45,7 @@ class ShakeDetector extends _$ShakeDetector { } }); - if (player.session == null) { + if (player.book == null) { _logger.config('No book is loaded, disabling shake detection'); wasPlayerLoaded = false; return null; @@ -87,7 +87,7 @@ class ShakeDetector extends _$ShakeDetector { required Ref ref, }) { final player = ref.read(playerProvider); - if (player.session == null && shakeAction.isPlaybackManagementEnabled) { + if (player.book == null && shakeAction.isPlaybackManagementEnabled) { _logger.warning('No book is loaded'); return false; } diff --git a/lib/features/shake_detector/shake_detector_provider.g.dart b/lib/features/shake_detector/shake_detector_provider.g.dart index a6934f5..5bc8440 100644 --- a/lib/features/shake_detector/shake_detector_provider.g.dart +++ b/lib/features/shake_detector/shake_detector_provider.g.dart @@ -6,7 +6,7 @@ part of 'shake_detector_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$shakeDetectorHash() => r'0e7881edd663f34e38abf7584dd648ec691aaca8'; +String _$shakeDetectorHash() => r'b63082b9016958e6c1e46ff874c98a0c99721f04'; /// See also [ShakeDetector]. @ProviderFor(ShakeDetector) diff --git a/lib/features/skip_start_end/providers/skip_start_end_provider.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.dart index 93e143b..e237b94 100644 --- a/lib/features/skip_start_end/providers/skip_start_end_provider.dart +++ b/lib/features/skip_start_end/providers/skip_start_end_provider.dart @@ -1,6 +1,7 @@ import 'package:riverpod_annotation/riverpod_annotation.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/currently_playing_provider.dart'; import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core; part 'skip_start_end_provider.g.dart'; @@ -9,9 +10,9 @@ part 'skip_start_end_provider.g.dart'; class SkipStartEnd extends _$SkipStartEnd { @override core.SkipStartEnd? build() { - final session = ref.watch(sessionProvider); - final bookId = session?.libraryItemId; - if (session == null || bookId == null) { + final currentBook = ref.watch(currentBookProvider); + final bookId = currentBook?.libraryItemId; + if (currentBook == null || bookId == null) { return null; } @@ -32,28 +33,3 @@ class SkipStartEnd extends _$SkipStartEnd { return skipStartEnd; } } - -// @riverpod -// class SkipStartEnd extends _$SkipStartEnd { -// @override -// core.SkipStartEnd? build() { -// final player = ref.watch(simpleAudiobookPlayerProvider); -// final book = ref.watch(audiobookPlayerProvider.select((v) => v.book)); -// final bookId = book?.libraryItemId ?? '_'; -// if (bookId == '_') { -// return null; -// } -// final bookSettings = ref.watch(bookSettingsProvider(bookId)); -// final start = bookSettings.playerSettings.skipChapterStart; -// final end = bookSettings.playerSettings.skipChapterEnd; - -// final skipStartEnd = core.SkipStartEnd( -// start: start, -// end: end, -// player: player, -// chapterId: player.currentChapter?.id, -// ); -// ref.onDispose(skipStartEnd.dispose); -// return skipStartEnd; -// } -// } diff --git a/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart b/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart index 4f5d2de..cffc000 100644 --- a/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart +++ b/lib/features/skip_start_end/providers/skip_start_end_provider.g.dart @@ -6,7 +6,7 @@ part of 'skip_start_end_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$skipStartEndHash() => r'6df119db598c6e8673dcea090ad97f5affab4016'; +String _$skipStartEndHash() => r'ba92dd22fc76f04cb5aaa220d025eb69c9d2ba46'; /// See also [SkipStartEnd]. @ProviderFor(SkipStartEnd) diff --git a/lib/features/skip_start_end/view/skip_start_end_button.dart b/lib/features/skip_start_end/view/skip_start_end_button.dart index 7d7236e..2e2be51 100644 --- a/lib/features/skip_start_end/view/skip_start_end_button.dart +++ b/lib/features/skip_start_end/view/skip_start_end_button.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:icons_plus/icons_plus.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/currently_playing_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart'; -import 'package:vaani/generated/l10n.dart'; import 'package:vaani/features/settings/view/notification_settings_page.dart'; +import 'package:vaani/generated/l10n.dart'; class SkipChapterStartEndButton extends HookConsumerWidget { const SkipChapterStartEndButton({super.key}); @@ -46,9 +46,9 @@ class PlayerSkipChapterStartEnd extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); - final bookId = session?.libraryItemId ?? '_'; - final bookSettings = ref.watch(bookSettingsProvider(bookId)); + final currentBook = ref.watch(currentBookProvider); + final bookId = currentBook?.libraryItemId ?? '_'; + final bookSettings = ref.read(bookSettingsProvider(bookId)); return Scaffold( body: Column( children: [ diff --git a/lib/main.dart b/lib/main.dart index 710b17b..bb30ff2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/features/logging/core/logger.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/settings/api_settings_provider.dart'; import 'package:vaani/features/settings/app_settings_provider.dart'; import 'package:vaani/framework.dart'; @@ -121,18 +122,18 @@ class AbsApp extends ConsumerWidget { if (themeSettings.useCurrentPlayerThemeThroughoutApp) { try { - final session = ref.watch(sessionProvider); - if (session != null) { + final currentBook = ref.watch(currentBookProvider); + if (currentBook != null) { final themeLight = ref.watch( themeOfLibraryItemProvider( - session.libraryItemId, + currentBook.libraryItemId, highContrast: shouldUseHighContrast, brightness: Brightness.light, ), ); final themeDark = ref.watch( themeOfLibraryItemProvider( - session.libraryItemId, + currentBook.libraryItemId, highContrast: shouldUseHighContrast, brightness: Brightness.dark, ), diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index bad1159..de9b6d6 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.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_desktop.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -10,18 +12,22 @@ class PlayerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final currentBook = ref.watch(currentBookProvider); + if (currentBook == null) { + return SizedBox.shrink(); + } final size = MediaQuery.of(context).size; // 竖屏 final isVertical = size.height > size.width; - return Scaffold( - appBar: AppBar( - shadowColor: Theme.of(context).colorScheme.onPrimary, + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(currentBook.metadata.title ?? ''), leading: IconButton( iconSize: 30, icon: const Icon(Icons.keyboard_arrow_down), onPressed: () => context.pop(), ), - actions: [ + trailingActions: [ IconButton( icon: const Icon(Icons.cast), onPressed: () { diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 85b672d..8f4187e 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/features/explore/providers/search_controller.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_minimized.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/generated/l10n.dart'; @@ -50,9 +50,10 @@ class ScaffoldWithNavBar extends HookConsumerWidget { Widget buildNavLeft(BuildContext context, WidgetRef ref) { // final isPlayerActive = ref.watch(isPlayerActiveProvider); - final session = ref.watch(sessionProvider); + final currentBook = ref.watch(currentBookProvider); return Padding( - padding: EdgeInsets.only(bottom: session != null ? playerMinHeight : 0), + padding: + EdgeInsets.only(bottom: currentBook != null ? playerMinHeight : 0), child: Row( children: [ SafeArea( diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 0c653da..1dfe8c7 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -11,6 +11,7 @@ import 'package:vaani/api/image_provider.dart'; import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_actions.dart'; +import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/providers/player_status_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/router/models/library_item_extras.dart'; @@ -214,10 +215,11 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(meProvider); // final player = ref.watch(audiobookPlayerProvider); - final session = ref.watch(sessionProvider); + final currentBook = ref.watch(currentBookProvider); final playerStatus = ref.watch(playerStatusProvider); final isLoading = playerStatus.isLoading(libraryItemId); - final isCurrentBookSetInPlayer = session?.libraryItemId == libraryItemId; + final isCurrentBookSetInPlayer = + currentBook?.libraryItemId == libraryItemId; final isPlayingThisBook = playerStatus.isPlaying() && isCurrentBookSetInPlayer; @@ -289,11 +291,13 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { .withValues(alpha: 0.9), ), ), - onPressed: () => session?.libraryItemId == libraryItemId - ? ref.read(playerProvider).togglePlayPause() - : ref - .read(sessionProvider.notifier) - .load(libraryItemId, null), + onPressed: () async { + final book = + await ref.watch(libraryItemProvider(libraryItemId).future); + + ref.read(currentBookProvider.notifier).update( + book.media.asBookExpanded, userProgress?.currentTime); + }, icon: Hero( tag: HeroTagPrefixes.libraryItemPlayButton + libraryItemId, child: DynamicItemPlayIcon( @@ -344,12 +348,12 @@ class BookCoverWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider); - if (session == null) { + final currentBook = ref.watch(currentBookProvider); + if (currentBook == null) { return const BookCoverSkeleton(); } final itemBeingPlayed = - ref.watch(libraryItemProvider(session.libraryItemId)); + ref.watch(libraryItemProvider(currentBook.libraryItemId)); final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null ? ref.watch( coverImageProvider(itemBeingPlayed.valueOrNull!.id),