mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-17 06:49:34 +00:00
123
This commit is contained in:
parent
178f3fbdb1
commit
634ffaed8c
27 changed files with 648 additions and 1012 deletions
|
|
@ -139,6 +139,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final book = ref.watch(currentBookProvider);
|
||||||
final player = ref.watch(absPlayerProvider);
|
final player = ref.watch(absPlayerProvider);
|
||||||
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull;
|
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull;
|
||||||
if (libraryItem == null) {
|
if (libraryItem == null) {
|
||||||
|
|
@ -146,13 +147,13 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
final mediaProgress = libraryItem.userMediaProgress;
|
final mediaProgress = libraryItem.userMediaProgress;
|
||||||
if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) {
|
if (mediaProgress == null && book?.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 (book?.libraryItemId == libraryItem.id) {
|
||||||
// final positionStream = useStream(player.slowPositionStream);
|
// final positionStream = useStream(player.slowPositionStream);
|
||||||
progress = (player.positionInBook).inSeconds /
|
progress = (player.positionInBook).inSeconds /
|
||||||
libraryItem.media.asBookExpanded.duration.inSeconds;
|
libraryItem.media.asBookExpanded.duration.inSeconds;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
||||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
import 'package:vaani/shared/utils/error_response.dart';
|
|
||||||
|
|
||||||
final _logger = Logger('PlaybackReporter');
|
final _logger = Logger('PlaybackReporter');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import 'package:just_audio/just_audio.dart';
|
|
||||||
|
|
||||||
class AudiobookPlayer extends AudioPlayer {}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
|
||||||
|
|
||||||
// 对接audio_service
|
|
||||||
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
|
||||||
final AbsAudioPlayer _player;
|
|
||||||
|
|
||||||
AbsAudioHandler(AbsAudioPlayer player) : _player = player {
|
|
||||||
player.mediaItemStream.listen((item) {
|
|
||||||
mediaItem.add(item);
|
|
||||||
});
|
|
||||||
// _player.playbackEventStream.map(_transformEvent).pipe(playbackState);
|
|
||||||
|
|
||||||
playbackState.add(
|
|
||||||
playbackState.value.copyWith(
|
|
||||||
controls: [
|
|
||||||
MediaControl.skipToPrevious,
|
|
||||||
// if (player.state.playing) MediaControl.pause else MediaControl.play,
|
|
||||||
// MediaControl.rewind,
|
|
||||||
// MediaControl.fastForward,
|
|
||||||
MediaControl.skipToNext,
|
|
||||||
MediaControl.stop,
|
|
||||||
],
|
|
||||||
systemActions: {
|
|
||||||
MediaAction.play,
|
|
||||||
MediaAction.pause,
|
|
||||||
MediaAction.seek,
|
|
||||||
MediaAction.seekForward,
|
|
||||||
MediaAction.seekBackward,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1. 转发播放/暂停状态
|
|
||||||
player.playerStateStream.listen((playerState) {
|
|
||||||
playbackState.add(
|
|
||||||
playbackState.value.copyWith(
|
|
||||||
playing: playerState.playing,
|
|
||||||
// 根据 playing 和实际情况更新 processingState
|
|
||||||
processingState: const {
|
|
||||||
AbsProcessingState.idle: AudioProcessingState.idle,
|
|
||||||
AbsProcessingState.loading: AudioProcessingState.loading,
|
|
||||||
AbsProcessingState.buffering: AudioProcessingState.buffering,
|
|
||||||
AbsProcessingState.ready: AudioProcessingState.ready,
|
|
||||||
AbsProcessingState.completed: AudioProcessingState.completed,
|
|
||||||
}[playerState.processingState] ??
|
|
||||||
AudioProcessingState.idle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// 2. 转发播放位置
|
|
||||||
player.positionStream.listen((Duration position) {
|
|
||||||
playbackState.add(
|
|
||||||
playbackState.value.copyWith(
|
|
||||||
updatePosition: position,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// 3. 转发媒体总时长
|
|
||||||
// player.stream.duration.listen((Duration? duration) {
|
|
||||||
// // 当有新的媒体加载时,更新 mediaItem 的 duration
|
|
||||||
// final currentItem = mediaItem.value;
|
|
||||||
// if (currentItem != null && duration != null) {
|
|
||||||
// mediaItem.add(currentItem.copyWith(duration: duration));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// player.stream.completed.listen((bool playing) {
|
|
||||||
// print('播放完成');
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放控制方法重写
|
|
||||||
@override
|
|
||||||
Future<void> play() async {
|
|
||||||
await _player.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() async {
|
|
||||||
await _player.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> skipToNext() async {
|
|
||||||
await _player.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> skipToPrevious() async {
|
|
||||||
await _player.previous();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position) async {
|
|
||||||
await _player.seek(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setSpeed(double speed) async {
|
|
||||||
await _player.setSpeed(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setVolume(double volume) async {
|
|
||||||
await _player.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
// if (!(_settingsProvider?['lockSeekingNotification'] ?? false))
|
|
||||||
// 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]!,
|
|
||||||
// playing: _player.playing,
|
|
||||||
// updatePosition: position,
|
|
||||||
// bufferedPosition: _player.bufferedPosition,
|
|
||||||
// speed: _player.speed,
|
|
||||||
// queueIndex: event.currentIndex,
|
|
||||||
// captioningEnabled: false,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.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:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
|
@ -16,20 +17,32 @@ final offset = Duration(milliseconds: 10);
|
||||||
|
|
||||||
final _logger = Logger('AbsAudioPlayer');
|
final _logger = Logger('AbsAudioPlayer');
|
||||||
|
|
||||||
/// 音频播放器抽象类
|
class AbsAudioPlayer {
|
||||||
abstract class AbsAudioPlayer {
|
late final AudioPlayer _player;
|
||||||
final _mediaItemController = BehaviorSubject<MediaItem?>.seeded(null);
|
AbsAudioPlayer(AudioPlayer player) : _player = player {
|
||||||
final playerStateSubject =
|
_player.positionStream.listen((position) {
|
||||||
BehaviorSubject.seeded(AbsPlayerState(false, AbsProcessingState.idle));
|
final chapter = currentChapter;
|
||||||
|
if (positionInBook <= (chapter?.start ?? Duration.zero) ||
|
||||||
|
positionInBook >= (chapter?.end ?? Duration.zero)) {
|
||||||
|
final chapter = book?.findChapterAtTime(positionInBook);
|
||||||
|
if (chapter != currentChapter) {
|
||||||
|
// print('当前章节时长: ${currentChapter?.duration}');
|
||||||
|
// print('切换章节时长: ${chapter?.duration}');
|
||||||
|
// print('当前播放音轨时长: ${_player.duration}');
|
||||||
|
chapterStreamController.add(chapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final _bookStreamController = BehaviorSubject<BookExpanded?>.seeded(null);
|
final _bookStreamController = BehaviorSubject<BookExpanded?>.seeded(null);
|
||||||
final chapterStreamController = BehaviorSubject<BookChapter?>.seeded(null);
|
final chapterStreamController = BehaviorSubject<BookChapter?>.seeded(null);
|
||||||
|
|
||||||
BookExpanded? get book => _bookStreamController.nvalue;
|
BookExpanded? get book => _bookStreamController.nvalue;
|
||||||
AudioTrack? get currentTrack => book?.tracks[currentIndex];
|
AudioTrack? get currentTrack => book?.tracks[currentIndex];
|
||||||
BookChapter? get currentChapter => chapterStreamController.nvalue;
|
BookChapter? get currentChapter => chapterStreamController.nvalue;
|
||||||
AbsPlayerState get playerState => playerStateSubject.value;
|
PlayerState get playerState => _player.playerState;
|
||||||
Stream<MediaItem?> get mediaItemStream => _mediaItemController.stream;
|
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
|
||||||
Stream<AbsPlayerState> get playerStateStream => playerStateSubject.stream;
|
|
||||||
|
|
||||||
// 加载整本书
|
// 加载整本书
|
||||||
Future<void> load(
|
Future<void> load(
|
||||||
|
|
@ -59,43 +72,77 @@ abstract class AbsAudioPlayer {
|
||||||
.formatNotificationTitle(book);
|
.formatNotificationTitle(book);
|
||||||
chapterStreamController
|
chapterStreamController
|
||||||
.add(book.findChapterAtTime(initialPosition ?? Duration.zero));
|
.add(book.findChapterAtTime(initialPosition ?? Duration.zero));
|
||||||
final item = MediaItem(
|
// final item = MediaItem(
|
||||||
id: book.libraryItemId,
|
// id: book.libraryItemId,
|
||||||
title: title,
|
// title: title,
|
||||||
artist: artist,
|
// artist: artist,
|
||||||
duration: currentChapter?.duration ?? book.duration,
|
// duration: currentChapter?.duration ?? book.duration,
|
||||||
artUri: Uri.parse(
|
// artUri: Uri.parse(
|
||||||
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
_mediaItemController.sink.add(item);
|
|
||||||
final playlist = book.tracks
|
mediaItem(track) => MediaItem(
|
||||||
.map(
|
id: book.libraryItemId + track.index.toString(),
|
||||||
(track) => (
|
title: title,
|
||||||
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
|
artist: artist,
|
||||||
track.duration
|
duration: currentChapter?.duration ?? book.duration,
|
||||||
|
artUri: Uri.parse(
|
||||||
|
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.toList();
|
List<AudioSource> audioSources = start != null && start > Duration.zero ||
|
||||||
await setPlayList(
|
end != null && end > Duration.zero
|
||||||
playlist,
|
? book.tracks
|
||||||
index: indexTrack,
|
.map(
|
||||||
position: positionInTrack,
|
(track) => ClippingAudioSource(
|
||||||
start: start,
|
child: AudioSource.uri(
|
||||||
end: end,
|
_getUri(
|
||||||
);
|
track,
|
||||||
|
downloadedUris,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
token: token,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
start: start,
|
||||||
|
end: end == null ? null : track.duration - end,
|
||||||
|
tag: mediaItem(track),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
: book.tracks
|
||||||
|
.map(
|
||||||
|
(track) => AudioSource.uri(
|
||||||
|
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
|
||||||
|
tag: mediaItem(track),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await _player
|
||||||
|
.setAudioSources(
|
||||||
|
audioSources,
|
||||||
|
preload: true,
|
||||||
|
initialIndex: indexTrack,
|
||||||
|
initialPosition: positionInTrack,
|
||||||
|
)
|
||||||
|
.catchError((error) {
|
||||||
|
_logger.shout('Error in setting audio source: $error');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setPlayList(
|
Future<void> play() async {
|
||||||
List<(Uri, Duration)> playlist, {
|
await _player.play();
|
||||||
int? index,
|
}
|
||||||
Duration? position,
|
|
||||||
Duration? start,
|
Future<void> pause() async {
|
||||||
Duration? end,
|
await _player.pause();
|
||||||
});
|
}
|
||||||
Future<void> play();
|
|
||||||
Future<void> pause();
|
Future<void> playOrPause() async {
|
||||||
Future<void> playOrPause();
|
_player.playing ? await _player.pause() : await _player.play();
|
||||||
|
}
|
||||||
|
|
||||||
// 跳到下一章
|
// 跳到下一章
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
|
|
@ -126,7 +173,19 @@ abstract class AbsAudioPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> seek(Duration position, {int? index});
|
Future<void> seek(Duration position, {int? index}) async {
|
||||||
|
await _player.seek(_addClippingStart(_player.position, add: false),
|
||||||
|
index: index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setSpeed(double speed) async {
|
||||||
|
await _player.setSpeed(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVolume(double volume) async {
|
||||||
|
await _player.setVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> seekInBook(Duration position) async {
|
Future<void> seekInBook(Duration position) async {
|
||||||
if (book == null) return;
|
if (book == null) return;
|
||||||
// 找到目标位置所在音轨和音轨内的位置
|
// 找到目标位置所在音轨和音轨内的位置
|
||||||
|
|
@ -140,8 +199,6 @@ abstract class AbsAudioPlayer {
|
||||||
await seek(positionInTrack, index: index);
|
await seek(positionInTrack, index: index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setSpeed(double speed);
|
|
||||||
Future<void> setVolume(double volume);
|
|
||||||
Future<void> switchChapter(int chapterId) async {
|
Future<void> switchChapter(int chapterId) async {
|
||||||
if (book == null) return;
|
if (book == null) return;
|
||||||
|
|
||||||
|
|
@ -153,15 +210,18 @@ abstract class AbsAudioPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get playing => playerState.playing;
|
bool get playing => playerState.playing;
|
||||||
Stream<bool> get playingStream;
|
Stream<bool> get playingStream => _player.playingStream;
|
||||||
Stream<BookExpanded?> get bookStream => _bookStreamController.stream;
|
Stream<BookExpanded?> get bookStream => _bookStreamController.stream;
|
||||||
Stream<BookChapter?> get chapterStream => chapterStreamController.stream;
|
Stream<BookChapter?> get chapterStream => chapterStreamController.stream;
|
||||||
|
|
||||||
int get currentIndex;
|
int get currentIndex => _player.currentIndex ?? 0;
|
||||||
double get speed;
|
double get speed => _player.speed;
|
||||||
|
|
||||||
Duration get position;
|
Duration get position => _addClippingStart(_player.position);
|
||||||
Stream<Duration> get positionStream;
|
Stream<Duration> get positionStream =>
|
||||||
|
_player.positionStream.where((_) => _player.playing).map((position) {
|
||||||
|
return _addClippingStart(position);
|
||||||
|
});
|
||||||
|
|
||||||
Duration get positionInChapter => getPositionInChapter(position);
|
Duration get positionInChapter => getPositionInChapter(position);
|
||||||
Duration getPositionInChapter(position) {
|
Duration getPositionInChapter(position) {
|
||||||
|
|
@ -183,8 +243,8 @@ abstract class AbsAudioPlayer {
|
||||||
return positionInBook;
|
return positionInBook;
|
||||||
});
|
});
|
||||||
|
|
||||||
Duration get bufferedPosition;
|
Duration get bufferedPosition => _player.bufferedPosition;
|
||||||
Stream<Duration> get bufferedPositionStream;
|
Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream;
|
||||||
Duration get bufferedPositionInBook =>
|
Duration get bufferedPositionInBook =>
|
||||||
bufferedPosition +
|
bufferedPosition +
|
||||||
(book?.tracks[currentIndex].startOffset ?? Duration.zero);
|
(book?.tracks[currentIndex].startOffset ?? Duration.zero);
|
||||||
|
|
@ -193,70 +253,26 @@ abstract class AbsAudioPlayer {
|
||||||
return bufferedPositionInBook;
|
return bufferedPositionInBook;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Duration _addClippingStart(Duration position, {bool add = true}) {
|
||||||
|
if (_player.sequenceState.currentSource != null &&
|
||||||
|
_player.sequenceState.currentSource is ClippingAudioSource) {
|
||||||
|
final currentSource =
|
||||||
|
_player.sequenceState.currentSource as ClippingAudioSource;
|
||||||
|
if (currentSource.start != null) {
|
||||||
|
return add
|
||||||
|
? position + currentSource.start!
|
||||||
|
: position - currentSource.start!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
_mediaItemController.close();
|
|
||||||
playerStateSubject.close();
|
|
||||||
_bookStreamController.close();
|
_bookStreamController.close();
|
||||||
chapterStreamController.close();
|
chapterStreamController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerates the different processing states of a player.
|
|
||||||
enum AbsProcessingState {
|
|
||||||
/// The player has not loaded an [AudioSource].
|
|
||||||
idle,
|
|
||||||
|
|
||||||
/// The player is loading an [AudioSource].
|
|
||||||
loading,
|
|
||||||
|
|
||||||
/// The player is buffering audio and unable to play.
|
|
||||||
buffering,
|
|
||||||
|
|
||||||
/// The player is has enough audio buffered and is able to play.
|
|
||||||
ready,
|
|
||||||
|
|
||||||
/// The player has reached the end of the audio.
|
|
||||||
completed,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encapsulates the playing and processing states. These two states vary
|
|
||||||
/// orthogonally, and so if [processingState] is [ProcessingState.buffering],
|
|
||||||
/// you can check [playing] to determine whether the buffering occurred while
|
|
||||||
/// the player was playing or while the player was paused.
|
|
||||||
class AbsPlayerState {
|
|
||||||
/// Whether the player will play when [processingState] is
|
|
||||||
/// [ProcessingState.ready].
|
|
||||||
final bool playing;
|
|
||||||
|
|
||||||
/// The current processing state of the player.
|
|
||||||
final AbsProcessingState processingState;
|
|
||||||
|
|
||||||
AbsPlayerState(this.playing, this.processingState);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'playing=$playing,processingState=$processingState';
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(playing, processingState);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other.runtimeType == runtimeType &&
|
|
||||||
other is PlayerState &&
|
|
||||||
other.playing == playing &&
|
|
||||||
other.processingState == processingState;
|
|
||||||
|
|
||||||
AbsPlayerState copyWith({
|
|
||||||
bool? playing,
|
|
||||||
AbsProcessingState? processingState,
|
|
||||||
}) {
|
|
||||||
return AbsPlayerState(
|
|
||||||
playing ?? this.playing,
|
|
||||||
processingState ?? this.processingState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri _getUri(
|
Uri _getUri(
|
||||||
AudioTrack track,
|
AudioTrack track,
|
||||||
List<Uri>? downloadedUris, {
|
List<Uri>? downloadedUris, {
|
||||||
|
|
@ -280,6 +296,38 @@ extension _ValueStreamExtension<T> on ValueStream<T> {
|
||||||
T? get nvalue => hasValue ? value : null;
|
T? get nvalue => hasValue ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension BookExpandedExtension on BookExpanded {
|
||||||
|
BookChapter findChapterAtTime(Duration position) {
|
||||||
|
return chapters.firstWhere(
|
||||||
|
(element) {
|
||||||
|
return element.start <= position && element.end >= position + offset;
|
||||||
|
},
|
||||||
|
orElse: () => chapters.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioTrack findTrackAtTime(Duration position) {
|
||||||
|
return tracks.firstWhere(
|
||||||
|
(element) {
|
||||||
|
return element.startOffset <= position &&
|
||||||
|
element.startOffset + element.duration >= position + offset;
|
||||||
|
},
|
||||||
|
orElse: () => tracks.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int findTrackIndexAtTime(Duration position) {
|
||||||
|
return tracks.indexWhere((element) {
|
||||||
|
return element.startOffset <= position &&
|
||||||
|
element.startOffset + element.duration >= position + offset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration getTrackStartOffset(int index) {
|
||||||
|
return tracks[index].startOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension FormatNotificationTitle on String {
|
extension FormatNotificationTitle on String {
|
||||||
String formatNotificationTitle(BookExpanded book) {
|
String formatNotificationTitle(BookExpanded book) {
|
||||||
return replaceAllMapped(
|
return replaceAllMapped(
|
||||||
|
|
@ -318,49 +366,3 @@ extension NotificationTitleUtils on NotificationTitleType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookExpandedExtension on BookExpanded {
|
|
||||||
BookChapter findChapterAtTime(Duration position) {
|
|
||||||
return chapters.firstWhere(
|
|
||||||
(element) {
|
|
||||||
return element.start <= position && element.end >= position + offset;
|
|
||||||
},
|
|
||||||
orElse: () => chapters.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioTrack findTrackAtTime(Duration position) {
|
|
||||||
return tracks.firstWhere(
|
|
||||||
(element) {
|
|
||||||
return element.startOffset <= position &&
|
|
||||||
element.startOffset + element.duration >= position + offset;
|
|
||||||
},
|
|
||||||
orElse: () => tracks.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
int findTrackIndexAtTime(Duration position) {
|
|
||||||
return tracks.indexWhere((element) {
|
|
||||||
return element.startOffset <= position &&
|
|
||||||
element.startOffset + element.duration >= position + offset;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration getTrackStartOffset(int index) {
|
|
||||||
return tracks[index].startOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AudioMetadata {
|
|
||||||
final String album;
|
|
||||||
final String title;
|
|
||||||
final String artist;
|
|
||||||
final String artwork;
|
|
||||||
|
|
||||||
AudioMetadata({
|
|
||||||
required this.album,
|
|
||||||
required this.title,
|
|
||||||
required this.artist,
|
|
||||||
required this.artwork,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' hide PlayerState;
|
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
|
||||||
|
|
||||||
/// 音频播放器 mpv全平台 (media_kit)
|
|
||||||
class AbsMpvAudioPlayer extends AbsAudioPlayer {
|
|
||||||
late Player player;
|
|
||||||
AbsMpvAudioPlayer() {
|
|
||||||
MediaKit.ensureInitialized();
|
|
||||||
player = Player();
|
|
||||||
player.stream.playing.listen((playing) {
|
|
||||||
final state = playerState;
|
|
||||||
playerStateSubject.add(
|
|
||||||
state.copyWith(
|
|
||||||
playing: playing,
|
|
||||||
processingState: playing
|
|
||||||
? state.processingState == AbsProcessingState.idle
|
|
||||||
? AbsProcessingState.ready
|
|
||||||
: state.processingState
|
|
||||||
: player.state.buffering
|
|
||||||
? AbsProcessingState.buffering
|
|
||||||
: player.state.completed
|
|
||||||
? AbsProcessingState.completed
|
|
||||||
: AbsProcessingState.ready,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Duration get bufferedPosition => player.state.buffer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Duration> get bufferedPositionStream => player.stream.buffer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get currentIndex => player.state.playlist.index;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() async {
|
|
||||||
await player.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> play() async {
|
|
||||||
await player.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> playOrPause() async {
|
|
||||||
await player.playOrPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<bool> get playingStream => player.stream.playing;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Duration get position => player.state.position;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Duration> get positionStream => player.stream.position;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position, {int? index}) async {
|
|
||||||
if (index != null) {
|
|
||||||
final playing = this.playing;
|
|
||||||
await player.jump(index);
|
|
||||||
if (!playing) await player.pause();
|
|
||||||
}
|
|
||||||
await player.seek(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setPlayList(
|
|
||||||
List<(Uri, Duration)> playlist, {
|
|
||||||
int? index,
|
|
||||||
Duration? position,
|
|
||||||
Duration? start,
|
|
||||||
Duration? end,
|
|
||||||
}) async {
|
|
||||||
await player.open(
|
|
||||||
Playlist(
|
|
||||||
playlist.map((uri) => Media(uri.$1.toString())).toList(),
|
|
||||||
index: index ?? 0,
|
|
||||||
),
|
|
||||||
play: false,
|
|
||||||
);
|
|
||||||
// 等待open方法加载完成
|
|
||||||
// ignore: unnecessary_null_comparison
|
|
||||||
await player.stream.duration.firstWhere((d) => d != null);
|
|
||||||
if (position != null) {
|
|
||||||
await player.seek(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setSpeed(double speed) async {
|
|
||||||
await player.setRate(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setVolume(double volume) async {
|
|
||||||
await player.setVolume(volume * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get speed => player.state.rate;
|
|
||||||
}
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
import 'package:just_audio/just_audio.dart';
|
|
||||||
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
|
||||||
|
|
||||||
final _logger = Logger('AbsPlatformAudioPlayer');
|
|
||||||
|
|
||||||
/// 音频播放器 平台ios,macos,android (just_audio)
|
|
||||||
class AbsPlatformAudioPlayer extends AbsAudioPlayer {
|
|
||||||
late final AudioPlayer _player;
|
|
||||||
AbsPlatformAudioPlayer() {
|
|
||||||
// 跳转到播放列表指定条目指定位置
|
|
||||||
// prefetch-playlist=yes
|
|
||||||
JustAudioMediaKit.prefetchPlaylist = true;
|
|
||||||
// merge-files=yes
|
|
||||||
// cache=yes
|
|
||||||
// cache-pause-wait=60
|
|
||||||
|
|
||||||
JustAudioMediaKit.ensureInitialized();
|
|
||||||
_player = AudioPlayer();
|
|
||||||
_player.playerStateStream.listen((state) {
|
|
||||||
playerStateSubject.add(
|
|
||||||
playerState.copyWith(
|
|
||||||
playing: state.playing,
|
|
||||||
processingState: {
|
|
||||||
ProcessingState.idle: AbsProcessingState.idle,
|
|
||||||
ProcessingState.buffering: AbsProcessingState.buffering,
|
|
||||||
ProcessingState.completed: AbsProcessingState.completed,
|
|
||||||
ProcessingState.loading: AbsProcessingState.loading,
|
|
||||||
ProcessingState.ready: AbsProcessingState.ready,
|
|
||||||
}[state.processingState],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
positionStream.listen((position) {
|
|
||||||
final chapter = currentChapter;
|
|
||||||
if (positionInBook <= (chapter?.start ?? Duration.zero) ||
|
|
||||||
positionInBook >= (chapter?.end ?? Duration.zero)) {
|
|
||||||
final chapter = book?.findChapterAtTime(positionInBook);
|
|
||||||
if (chapter != currentChapter) {
|
|
||||||
// print('当前章节时长: ${currentChapter?.duration}');
|
|
||||||
// print('切换章节时长: ${chapter?.duration}');
|
|
||||||
// print('当前播放音轨时长: ${_player.duration}');
|
|
||||||
chapterStreamController.add(chapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Duration get bufferedPosition => _player.bufferedPosition;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream
|
|
||||||
.where(
|
|
||||||
(_) => _player.playerState.processingState == ProcessingState.buffering,
|
|
||||||
)
|
|
||||||
.asBroadcastStream();
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get currentIndex => _player.currentIndex ?? 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() async {
|
|
||||||
await _player.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> play() async {
|
|
||||||
await _player.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> playOrPause() async {
|
|
||||||
_player.playing ? await _player.pause() : await _player.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<bool> get playingStream => _player.playingStream;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Duration get position => _addClippingStart(_player.position);
|
|
||||||
|
|
||||||
Duration _addClippingStart(Duration position, {bool add = true}) {
|
|
||||||
if (_player.sequenceState.currentSource != null &&
|
|
||||||
_player.sequenceState.currentSource is ClippingAudioSource) {
|
|
||||||
final currentSource =
|
|
||||||
_player.sequenceState.currentSource as ClippingAudioSource;
|
|
||||||
if (currentSource.start != null) {
|
|
||||||
return add
|
|
||||||
? position + currentSource.start!
|
|
||||||
: position - currentSource.start!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Duration> get positionStream =>
|
|
||||||
_player.positionStream.where((_) => _player.playing).map((position) {
|
|
||||||
return _addClippingStart(position);
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position, {int? index}) async {
|
|
||||||
await _player.seek(_addClippingStart(position, add: false), index: index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setPlayList(
|
|
||||||
List<(Uri, Duration)> playlist, {
|
|
||||||
int? index,
|
|
||||||
Duration? position,
|
|
||||||
Duration? start,
|
|
||||||
Duration? end,
|
|
||||||
}) async {
|
|
||||||
List<AudioSource> audioSources = start != null && start > Duration.zero ||
|
|
||||||
end != null && end > Duration.zero
|
|
||||||
? playlist
|
|
||||||
.map(
|
|
||||||
(item) => ClippingAudioSource(
|
|
||||||
child: AudioSource.uri(item.$1),
|
|
||||||
start: start,
|
|
||||||
end: end == null ? null : item.$2 - end,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
: playlist.map((item) => AudioSource.uri(item.$1)).toList();
|
|
||||||
await _player
|
|
||||||
.setAudioSources(
|
|
||||||
audioSources,
|
|
||||||
preload: true,
|
|
||||||
initialIndex: index,
|
|
||||||
initialPosition: position,
|
|
||||||
)
|
|
||||||
.catchError((error) {
|
|
||||||
_logger.shout('Error in setting audio source: $error');
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setSpeed(double speed) async {
|
|
||||||
await _player.setSpeed(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setVolume(double volume) async {
|
|
||||||
await _player.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get speed => _player.speed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_player.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +1,69 @@
|
||||||
// import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
// import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
// import 'package:just_audio_background/just_audio_background.dart'
|
import 'package:just_audio_background/just_audio_background.dart'
|
||||||
// show JustAudioBackground, NotificationConfig;
|
show JustAudioBackground, NotificationConfig;
|
||||||
// import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
||||||
// show JustAudioMediaKit;
|
show JustAudioMediaKit;
|
||||||
// import 'package:vaani/features/settings/app_settings_provider.dart';
|
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||||
// import 'package:vaani/features/settings/models/app_settings.dart';
|
import 'package:vaani/features/settings/models/app_settings.dart';
|
||||||
|
|
||||||
// Future<void> configurePlayer() async {
|
Future<void> configurePlayer() async {
|
||||||
// // for playing audio on windows, linux
|
// for playing audio on windows, linux
|
||||||
// JustAudioMediaKit.ensureInitialized();
|
JustAudioMediaKit.ensureInitialized();
|
||||||
|
|
||||||
// // for configuring how this app will interact with other audio apps
|
// 跳转到播放列表指定条目指定位置
|
||||||
// final session = await AudioSession.instance;
|
// prefetch-playlist=yes
|
||||||
// await session.configure(const AudioSessionConfiguration.speech());
|
// JustAudioMediaKit.prefetchPlaylist = true;
|
||||||
|
// merge-files=yes
|
||||||
|
// cache=yes
|
||||||
|
// cache-pause-wait=60
|
||||||
|
|
||||||
// final appSettings = loadOrCreateAppSettings();
|
// for configuring how this app will interact with other audio apps
|
||||||
|
final session = await AudioSession.instance;
|
||||||
|
await session.configure(const AudioSessionConfiguration.speech());
|
||||||
|
|
||||||
// // for playing audio in the background
|
final appSettings = loadOrCreateAppSettings();
|
||||||
// await JustAudioBackground.init(
|
|
||||||
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
// for playing audio in the background
|
||||||
// androidNotificationChannelName: 'Audio playback',
|
await JustAudioBackground.init(
|
||||||
// androidNotificationOngoing: false,
|
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||||
// androidStopForegroundOnPause: false,
|
androidNotificationChannelName: 'Audio playback',
|
||||||
// androidNotificationChannelDescription: 'Audio playback in the background',
|
androidNotificationOngoing: false,
|
||||||
// androidNotificationIcon: 'drawable/ic_stat_logo',
|
androidStopForegroundOnPause: false,
|
||||||
// rewindInterval: appSettings.notificationSettings.rewindInterval,
|
androidNotificationChannelDescription: 'Audio playback in the background',
|
||||||
// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
|
androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||||
// androidShowNotificationBadge: false,
|
rewindInterval: appSettings.notificationSettings.rewindInterval,
|
||||||
// notificationConfigBuilder: (state) {
|
fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
|
||||||
// final controls = [
|
androidShowNotificationBadge: false,
|
||||||
// if (appSettings.notificationSettings.mediaControls
|
notificationConfigBuilder: (state) {
|
||||||
// .contains(NotificationMediaControl.skipToPreviousChapter) &&
|
final controls = [
|
||||||
// state.hasPrevious)
|
if (appSettings.notificationSettings.mediaControls
|
||||||
// MediaControl.skipToPrevious,
|
.contains(NotificationMediaControl.skipToPreviousChapter) &&
|
||||||
// if (appSettings.notificationSettings.mediaControls
|
state.hasPrevious)
|
||||||
// .contains(NotificationMediaControl.rewind))
|
MediaControl.skipToPrevious,
|
||||||
// MediaControl.rewind,
|
if (appSettings.notificationSettings.mediaControls
|
||||||
// if (state.playing) MediaControl.pause else MediaControl.play,
|
.contains(NotificationMediaControl.rewind))
|
||||||
// if (appSettings.notificationSettings.mediaControls
|
MediaControl.rewind,
|
||||||
// .contains(NotificationMediaControl.fastForward))
|
if (state.playing) MediaControl.pause else MediaControl.play,
|
||||||
// MediaControl.fastForward,
|
if (appSettings.notificationSettings.mediaControls
|
||||||
// if (appSettings.notificationSettings.mediaControls
|
.contains(NotificationMediaControl.fastForward))
|
||||||
// .contains(NotificationMediaControl.skipToNextChapter) &&
|
MediaControl.fastForward,
|
||||||
// state.hasNext)
|
if (appSettings.notificationSettings.mediaControls
|
||||||
// MediaControl.skipToNext,
|
.contains(NotificationMediaControl.skipToNextChapter) &&
|
||||||
// if (appSettings.notificationSettings.mediaControls
|
state.hasNext)
|
||||||
// .contains(NotificationMediaControl.stop))
|
MediaControl.skipToNext,
|
||||||
// MediaControl.stop,
|
if (appSettings.notificationSettings.mediaControls
|
||||||
// ];
|
.contains(NotificationMediaControl.stop))
|
||||||
// return NotificationConfig(
|
MediaControl.stop,
|
||||||
// controls: controls,
|
];
|
||||||
// systemActions: const {
|
return NotificationConfig(
|
||||||
// MediaAction.seek,
|
controls: controls,
|
||||||
// MediaAction.seekForward,
|
systemActions: const {
|
||||||
// MediaAction.seekBackward,
|
MediaAction.seek,
|
||||||
// },
|
MediaAction.seekForward,
|
||||||
// );
|
MediaAction.seekBackward,
|
||||||
// },
|
},
|
||||||
// );
|
);
|
||||||
// }
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:audio_session/audio_session.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart' as audio;
|
||||||
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 api;
|
import 'package:shelfsdk/audiobookshelf_api.dart' as api;
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
|
|
@ -12,55 +9,52 @@ import 'package:vaani/db/available_boxes.dart';
|
||||||
import 'package:vaani/db/cache/cache_key.dart';
|
import 'package:vaani/db/cache/cache_key.dart';
|
||||||
import 'package:vaani/features/downloads/providers/download_manager.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/per_book_settings/providers/book_settings_provider.dart';
|
||||||
import 'package:vaani/features/player/core/abs_audio_handler.dart';
|
import 'package:vaani/features/player/core/abs_audio_player.dart'
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart' as core;
|
show AbsAudioPlayer;
|
||||||
import 'package:vaani/features/player/core/abs_audio_player_platform.dart';
|
|
||||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/box.dart';
|
import 'package:vaani/shared/extensions/box.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
|
||||||
part 'abs_provider.g.dart';
|
part 'abs_provider.g.dart';
|
||||||
|
|
||||||
final _logger = Logger('AbsPlayerProvider');
|
|
||||||
|
|
||||||
/// 音频播放器 配置
|
/// 音频播放器 配置
|
||||||
@Riverpod(keepAlive: true)
|
// @Riverpod(keepAlive: true)
|
||||||
Future<AudioHandler> configurePlayer(Ref ref) async {
|
// Future<AudioHandler> configurePlayer(Ref ref) async {
|
||||||
final player = ref.read(absPlayerProvider);
|
// final player = ref.read(absPlayerProvider);
|
||||||
// for playing audio on windows, linux
|
// // for playing audio on windows, linux
|
||||||
|
|
||||||
// for configuring how this app will interact with other audio apps
|
// // for configuring how this app will interact with other audio apps
|
||||||
final session = await AudioSession.instance;
|
// final session = await AudioSession.instance;
|
||||||
await session.configure(const AudioSessionConfiguration.speech());
|
// await session.configure(const AudioSessionConfiguration.speech());
|
||||||
|
|
||||||
final audioService = await AudioService.init(
|
// final audioService = await AudioService.init(
|
||||||
builder: () => AbsAudioHandler(player),
|
// builder: () => AbsAudioHandler(player),
|
||||||
config: const AudioServiceConfig(
|
// config: const AudioServiceConfig(
|
||||||
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||||
androidNotificationChannelName: 'ABSPlayback',
|
// androidNotificationChannelName: 'ABSPlayback',
|
||||||
androidNotificationChannelDescription:
|
// androidNotificationChannelDescription:
|
||||||
'Needed to control audio from lock screen',
|
// 'Needed to control audio from lock screen',
|
||||||
androidNotificationOngoing: false,
|
// androidNotificationOngoing: false,
|
||||||
androidStopForegroundOnPause: false,
|
// androidStopForegroundOnPause: false,
|
||||||
androidNotificationIcon: 'drawable/ic_stat_logo',
|
// androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||||
preloadArtwork: true,
|
// preloadArtwork: true,
|
||||||
// fastForwardInterval: Duration(seconds: 20),
|
// // fastForwardInterval: Duration(seconds: 20),
|
||||||
// rewindInterval: Duration(seconds: 20),
|
// // rewindInterval: Duration(seconds: 20),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
|
|
||||||
_logger.finer('created simple player');
|
// _logger.finer('created simple player');
|
||||||
return audioService;
|
// return audioService;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// just_audio 播放器
|
// just_audio 播放器
|
||||||
@Riverpod(keepAlive: true)
|
// @Riverpod(keepAlive: true)
|
||||||
core.AbsAudioPlayer audioPlayer(Ref ref) {
|
// core.AbsAudioPlayer audioPlayer(Ref ref) {
|
||||||
final player = AbsPlatformAudioPlayer();
|
// final player = AbsPlatformAudioPlayer();
|
||||||
// final player = AbsMpvAudioPlayer();
|
// // final player = AbsMpvAudioPlayer();
|
||||||
ref.onDispose(player.dispose);
|
// ref.onDispose(player.dispose);
|
||||||
return player;
|
// return player;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 播放器激活状态
|
// 播放器激活状态
|
||||||
@riverpod
|
@riverpod
|
||||||
|
|
@ -69,129 +63,20 @@ bool playerActive(Ref ref) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
AudioPlayer simpleAudioPlayer(Ref ref) {
|
audio.AudioPlayer simpleAudioPlayer(Ref ref) {
|
||||||
final player = AudioPlayer();
|
final player = audio.AudioPlayer();
|
||||||
ref.onDispose(player.dispose);
|
ref.onDispose(player.dispose);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
final offset = Duration(milliseconds: 10);
|
||||||
class AbsAudioPlayer extends _$AbsAudioPlayer {
|
|
||||||
@override
|
|
||||||
AudioPlayer build() {
|
|
||||||
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
|
|
||||||
return audioPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> load(
|
|
||||||
api.BookExpanded book, {
|
|
||||||
Duration? initialPosition,
|
|
||||||
bool play = true,
|
|
||||||
}) async {
|
|
||||||
final currentTrack = book.findTrackAtTime(initialPosition ?? Duration.zero);
|
|
||||||
final indexTrack = book.tracks.indexOf(currentTrack);
|
|
||||||
final positionInTrack = initialPosition != null
|
|
||||||
? initialPosition - currentTrack.startOffset
|
|
||||||
: null;
|
|
||||||
final api = ref.read(authenticatedApiProvider);
|
|
||||||
|
|
||||||
final downloadManager = ref.read(simpleDownloadManagerProvider);
|
|
||||||
print(downloadManager.basePath);
|
|
||||||
|
|
||||||
final libItem =
|
|
||||||
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
|
||||||
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
|
||||||
|
|
||||||
final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId));
|
|
||||||
var bookPlayerSettings = bookSettings.playerSettings;
|
|
||||||
final start = bookSettings.playerSettings.skipChapterStart;
|
|
||||||
final end = bookSettings.playerSettings.skipChapterEnd;
|
|
||||||
final appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
|
|
||||||
final configurePlayerForEveryBook =
|
|
||||||
appPlayerSettings.configurePlayerForEveryBook;
|
|
||||||
List<AudioSource> audioSources =
|
|
||||||
start > Duration.zero || end > Duration.zero
|
|
||||||
? book.tracks
|
|
||||||
.map(
|
|
||||||
(track) => ClippingAudioSource(
|
|
||||||
child: AudioSource.uri(
|
|
||||||
_getUri(
|
|
||||||
track,
|
|
||||||
downloadedUris,
|
|
||||||
baseUrl: api.baseUrl,
|
|
||||||
token: api.token!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
start: start,
|
|
||||||
end: end > Duration.zero ? null : track.duration - end,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
: book.tracks
|
|
||||||
.map(
|
|
||||||
(track) => AudioSource.uri(
|
|
||||||
_getUri(
|
|
||||||
track,
|
|
||||||
downloadedUris,
|
|
||||||
baseUrl: api.baseUrl,
|
|
||||||
token: api.token!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
await state
|
|
||||||
.setAudioSources(
|
|
||||||
audioSources,
|
|
||||||
preload: true,
|
|
||||||
initialIndex: indexTrack,
|
|
||||||
initialPosition: positionInTrack,
|
|
||||||
)
|
|
||||||
.catchError((error) {
|
|
||||||
_logger.shout('Error in setting audio source: $error');
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
// set the volume
|
|
||||||
await state.setVolume(
|
|
||||||
configurePlayerForEveryBook
|
|
||||||
? bookPlayerSettings.preferredDefaultVolume ??
|
|
||||||
appPlayerSettings.preferredDefaultVolume
|
|
||||||
: appPlayerSettings.preferredDefaultVolume,
|
|
||||||
);
|
|
||||||
// set the speed
|
|
||||||
await state.setSpeed(
|
|
||||||
configurePlayerForEveryBook
|
|
||||||
? bookPlayerSettings.preferredDefaultSpeed ??
|
|
||||||
appPlayerSettings.preferredDefaultSpeed
|
|
||||||
: appPlayerSettings.preferredDefaultSpeed,
|
|
||||||
);
|
|
||||||
if (play) await state.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri _getUri(
|
|
||||||
api.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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 音频播放器 riverpod状态
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class AbsPlayer extends _$AbsPlayer {
|
class AbsPlayer extends _$AbsPlayer {
|
||||||
@override
|
@override
|
||||||
core.AbsAudioPlayer build() {
|
AbsAudioPlayer build() {
|
||||||
final audioPlayer = ref.watch(audioPlayerProvider);
|
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
|
||||||
return audioPlayer;
|
return AbsAudioPlayer(audioPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load(
|
Future<void> load(
|
||||||
|
|
@ -246,10 +131,71 @@ class AbsPlayer extends _$AbsPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 音频播放器 riverpod状态
|
||||||
|
// @Riverpod(keepAlive: true)
|
||||||
|
// class AbsPlayer extends _$AbsPlayer {
|
||||||
|
// @override
|
||||||
|
// core.AbsAudioPlayer build() {
|
||||||
|
// final audioPlayer = ref.watch(audioPlayerProvider);
|
||||||
|
// return audioPlayer;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<void> load(
|
||||||
|
// api.BookExpanded book, {
|
||||||
|
// Duration? initialPosition,
|
||||||
|
// bool play = true,
|
||||||
|
// }) async {
|
||||||
|
// if (state.book == book || state.book?.libraryItemId == book.libraryItemId) {
|
||||||
|
// state.playOrPause();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// final api = ref.read(authenticatedApiProvider);
|
||||||
|
|
||||||
|
// final downloadManager = ref.read(simpleDownloadManagerProvider);
|
||||||
|
// print(downloadManager.basePath);
|
||||||
|
|
||||||
|
// final libItem =
|
||||||
|
// await ref.read(libraryItemProvider(book.libraryItemId).future);
|
||||||
|
// final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
||||||
|
|
||||||
|
// final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId));
|
||||||
|
// var bookPlayerSettings = bookSettings.playerSettings;
|
||||||
|
// var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
|
||||||
|
|
||||||
|
// var configurePlayerForEveryBook =
|
||||||
|
// appPlayerSettings.configurePlayerForEveryBook;
|
||||||
|
|
||||||
|
// await state.load(
|
||||||
|
// book,
|
||||||
|
// baseUrl: api.baseUrl,
|
||||||
|
// token: api.token!,
|
||||||
|
// initialPosition: initialPosition,
|
||||||
|
// downloadedUris: downloadedUris,
|
||||||
|
// start: bookSettings.playerSettings.skipChapterStart,
|
||||||
|
// end: bookSettings.playerSettings.skipChapterEnd,
|
||||||
|
// );
|
||||||
|
// // set the volume
|
||||||
|
// await state.setVolume(
|
||||||
|
// configurePlayerForEveryBook
|
||||||
|
// ? bookPlayerSettings.preferredDefaultVolume ??
|
||||||
|
// appPlayerSettings.preferredDefaultVolume
|
||||||
|
// : appPlayerSettings.preferredDefaultVolume,
|
||||||
|
// );
|
||||||
|
// // set the speed
|
||||||
|
// await state.setSpeed(
|
||||||
|
// configurePlayerForEveryBook
|
||||||
|
// ? bookPlayerSettings.preferredDefaultSpeed ??
|
||||||
|
// appPlayerSettings.preferredDefaultSpeed
|
||||||
|
// : appPlayerSettings.preferredDefaultSpeed,
|
||||||
|
// );
|
||||||
|
// if (play) await state.play();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class PlayerState extends _$PlayerState {
|
class PlayerState extends _$PlayerState {
|
||||||
@override
|
@override
|
||||||
core.AbsPlayerState build() {
|
audio.PlayerState build() {
|
||||||
final player = ref.read(absPlayerProvider);
|
final player = ref.read(absPlayerProvider);
|
||||||
player.playerStateStream.listen((playerState) {
|
player.playerStateStream.listen((playerState) {
|
||||||
if (playerState != state) {
|
if (playerState != state) {
|
||||||
|
|
@ -260,10 +206,10 @@ class PlayerState extends _$PlayerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLoading(String itemId) {
|
bool isLoading(String itemId) {
|
||||||
final player = ref.read(absPlayerProvider);
|
final book = ref.read(currentBookProvider);
|
||||||
return player.book?.libraryItemId == itemId &&
|
return book?.libraryItemId == itemId &&
|
||||||
!state.playing &&
|
!state.playing &&
|
||||||
state.processingState == core.AbsProcessingState.loading;
|
state.processingState == audio.ProcessingState.loading;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPlaying() {
|
bool isPlaying() {
|
||||||
|
|
@ -307,7 +253,7 @@ class CurrentBook extends _$CurrentBook {
|
||||||
|
|
||||||
Future<void> update(String libraryItemId, {bool play = true}) async {
|
Future<void> update(String libraryItemId, {bool play = true}) async {
|
||||||
if (state?.libraryItemId == libraryItemId) {
|
if (state?.libraryItemId == libraryItemId) {
|
||||||
ref.read(audioPlayerProvider).playOrPause();
|
ref.read(absPlayerProvider).playOrPause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final book = await ref.read(libraryItemProvider(libraryItemId).future);
|
final book = await ref.read(libraryItemProvider(libraryItemId).future);
|
||||||
|
|
|
||||||
|
|
@ -6,44 +6,11 @@ part of 'abs_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$configurePlayerHash() => r'7ac63b6c3a34c56f42be55bc7a4856dabaae1583';
|
String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910';
|
||||||
|
|
||||||
/// 音频播放器 配置
|
/// 音频播放器 配置
|
||||||
///
|
///
|
||||||
/// Copied from [configurePlayer].
|
/// Copied from [playerActive].
|
||||||
@ProviderFor(configurePlayer)
|
|
||||||
final configurePlayerProvider = FutureProvider<AudioHandler>.internal(
|
|
||||||
configurePlayer,
|
|
||||||
name: r'configurePlayerProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$configurePlayerHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
|
||||||
// ignore: unused_element
|
|
||||||
typedef ConfigurePlayerRef = FutureProviderRef<AudioHandler>;
|
|
||||||
String _$audioPlayerHash() => r'156f85effafdcd287db88e455e8f4f4d33c41a0e';
|
|
||||||
|
|
||||||
/// See also [audioPlayer].
|
|
||||||
@ProviderFor(audioPlayer)
|
|
||||||
final audioPlayerProvider = Provider<core.AbsAudioPlayer>.internal(
|
|
||||||
audioPlayer,
|
|
||||||
name: r'audioPlayerProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$audioPlayerHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
|
||||||
// ignore: unused_element
|
|
||||||
typedef AudioPlayerRef = ProviderRef<core.AbsAudioPlayer>;
|
|
||||||
String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910';
|
|
||||||
|
|
||||||
/// See also [playerActive].
|
|
||||||
@ProviderFor(playerActive)
|
@ProviderFor(playerActive)
|
||||||
final playerActiveProvider = AutoDisposeProvider<bool>.internal(
|
final playerActiveProvider = AutoDisposeProvider<bool>.internal(
|
||||||
playerActive,
|
playerActive,
|
||||||
|
|
@ -57,11 +24,11 @@ final playerActiveProvider = AutoDisposeProvider<bool>.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 PlayerActiveRef = AutoDisposeProviderRef<bool>;
|
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
|
||||||
String _$simpleAudioPlayerHash() => r'4da667e3b7047003edd594f8a76700afb963aceb';
|
String _$simpleAudioPlayerHash() => r'99d84a750cf605ad036603320925f0ba7253930b';
|
||||||
|
|
||||||
/// See also [simpleAudioPlayer].
|
/// See also [simpleAudioPlayer].
|
||||||
@ProviderFor(simpleAudioPlayer)
|
@ProviderFor(simpleAudioPlayer)
|
||||||
final simpleAudioPlayerProvider = Provider<AudioPlayer>.internal(
|
final simpleAudioPlayerProvider = Provider<audio.AudioPlayer>.internal(
|
||||||
simpleAudioPlayer,
|
simpleAudioPlayer,
|
||||||
name: r'simpleAudioPlayerProvider',
|
name: r'simpleAudioPlayerProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
|
@ -73,7 +40,7 @@ final simpleAudioPlayerProvider = Provider<AudioPlayer>.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 SimpleAudioPlayerRef = ProviderRef<AudioPlayer>;
|
typedef SimpleAudioPlayerRef = ProviderRef<audio.AudioPlayer>;
|
||||||
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
|
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
|
|
@ -242,30 +209,11 @@ final positionChapterProvider = AutoDisposeStreamProvider<Duration>.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 PositionChapterRef = AutoDisposeStreamProviderRef<Duration>;
|
typedef PositionChapterRef = AutoDisposeStreamProviderRef<Duration>;
|
||||||
String _$absAudioPlayerHash() => r'f595b5033eed9f4a4aa07c297c4a176955e6aab1';
|
String _$absPlayerHash() => r'370f576d3d3a2196d1a93f2046005c1a3298d994';
|
||||||
|
|
||||||
/// See also [AbsAudioPlayer].
|
/// See also [AbsPlayer].
|
||||||
@ProviderFor(AbsAudioPlayer)
|
|
||||||
final absAudioPlayerProvider =
|
|
||||||
NotifierProvider<AbsAudioPlayer, AudioPlayer>.internal(
|
|
||||||
AbsAudioPlayer.new,
|
|
||||||
name: r'absAudioPlayerProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$absAudioPlayerHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$AbsAudioPlayer = Notifier<AudioPlayer>;
|
|
||||||
String _$absPlayerHash() => r'e682fea03793a0370cb143602980d5c1e37396c7';
|
|
||||||
|
|
||||||
/// 音频播放器 riverpod状态
|
|
||||||
///
|
|
||||||
/// Copied from [AbsPlayer].
|
|
||||||
@ProviderFor(AbsPlayer)
|
@ProviderFor(AbsPlayer)
|
||||||
final absPlayerProvider =
|
final absPlayerProvider = NotifierProvider<AbsPlayer, AbsAudioPlayer>.internal(
|
||||||
NotifierProvider<AbsPlayer, core.AbsAudioPlayer>.internal(
|
|
||||||
AbsPlayer.new,
|
AbsPlayer.new,
|
||||||
name: r'absPlayerProvider',
|
name: r'absPlayerProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
|
|
@ -274,13 +222,15 @@ final absPlayerProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$AbsPlayer = Notifier<core.AbsAudioPlayer>;
|
typedef _$AbsPlayer = Notifier<AbsAudioPlayer>;
|
||||||
String _$playerStateHash() => r'f195d2d13bcee0f91b862e669ab3549667d8dd2d';
|
String _$playerStateHash() => r'eb79bd816714f721da1c4226d4447de5dc55fc5c';
|
||||||
|
|
||||||
/// See also [PlayerState].
|
/// 音频播放器 riverpod状态
|
||||||
|
///
|
||||||
|
/// Copied from [PlayerState].
|
||||||
@ProviderFor(PlayerState)
|
@ProviderFor(PlayerState)
|
||||||
final playerStateProvider =
|
final playerStateProvider =
|
||||||
AutoDisposeNotifierProvider<PlayerState, core.AbsPlayerState>.internal(
|
AutoDisposeNotifierProvider<PlayerState, audio.PlayerState>.internal(
|
||||||
PlayerState.new,
|
PlayerState.new,
|
||||||
name: r'playerStateProvider',
|
name: r'playerStateProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
|
|
@ -289,8 +239,8 @@ final playerStateProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
|
typedef _$PlayerState = AutoDisposeNotifier<audio.PlayerState>;
|
||||||
String _$currentBookHash() => r'714d7701508b6186598e13bc38c57c3fe644ae90';
|
String _$currentBookHash() => r'85de9041d356e214761b65bd1b7b74321d5a9221';
|
||||||
|
|
||||||
/// See also [CurrentBook].
|
/// See also [CurrentBook].
|
||||||
@ProviderFor(CurrentBook)
|
@ProviderFor(CurrentBook)
|
||||||
|
|
|
||||||
|
|
@ -62,17 +62,17 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
// add a shadow to the image elevation hovering effect
|
// add a shadow to the image elevation hovering effect
|
||||||
child: PlayerExpandedImage(imageSize),
|
child: PlayerExpandedImage(imageSize),
|
||||||
),
|
),
|
||||||
_buildControls(imageSize),
|
// _buildControls(imageSize),
|
||||||
SizedBox(
|
// 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 AudiobookChapterProgressBar(),
|
// child: const AudiobookChapterProgressBar(),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
_buildSettings(imageSize),
|
_buildSettings(imageSize),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -113,7 +113,37 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Hero(tag: 'player_hero', child: const PlayerMinimizedControls()),
|
SizedBox(
|
||||||
|
height: playerMinimizedHeight,
|
||||||
|
child: _buildBottom(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottom() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const AudiobookPlayerSeekChapterButton(isForward: false),
|
||||||
|
// play/pause button
|
||||||
|
const AudiobookPlayerPlayPauseButton(),
|
||||||
|
const AudiobookPlayerSeekChapterButton(isForward: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: AppElementSizes.paddingRegular,
|
||||||
|
right: AppElementSizes.paddingRegular,
|
||||||
|
),
|
||||||
|
child: const AudiobookChapterProgressBar(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +158,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
const AudiobookPlayerSeekChapterButton(isForward: false),
|
const AudiobookPlayerSeekChapterButton(isForward: false),
|
||||||
// buttonSkipBackwards
|
// buttonSkipBackwards
|
||||||
const AudiobookPlayerSeekButton(isForward: false),
|
const AudiobookPlayerSeekButton(isForward: false),
|
||||||
AudiobookPlayerPlayPauseButton(),
|
const AudiobookPlayerPlayPauseButton(),
|
||||||
// // buttonSkipForwards
|
// // buttonSkipForwards
|
||||||
const AudiobookPlayerSeekButton(isForward: true),
|
const AudiobookPlayerSeekButton(isForward: true),
|
||||||
// // next chapter
|
// // next chapter
|
||||||
|
|
@ -144,6 +174,8 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
|
const AudiobookPlayerSeekButton(isForward: false),
|
||||||
|
const AudiobookPlayerSeekButton(isForward: true),
|
||||||
// speed control
|
// speed control
|
||||||
const PlayerSpeedAdjustButton(),
|
const PlayerSpeedAdjustButton(),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,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/constants/sizes.dart';
|
import 'package:vaani/constants/sizes.dart';
|
||||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
import 'package:vaani/features/player/providers/abs_provider.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_player_pause_button.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/shared/extensions/chapter.dart';
|
import 'package:vaani/shared/extensions/chapter.dart';
|
||||||
|
|
@ -22,6 +23,9 @@ class PlayerMinimized extends HookConsumerWidget {
|
||||||
if (currentBook == null) {
|
if (currentBook == null) {
|
||||||
return SizedBox.shrink();
|
return SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
// 竖屏
|
||||||
|
final isVertical = size.height > size.width;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: playerMinimizedHeight,
|
height: playerMinimizedHeight,
|
||||||
|
|
@ -29,8 +33,10 @@ class PlayerMinimized extends HookConsumerWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
children: [
|
children: [
|
||||||
Hero(tag: 'player_hero', child: const PlayerMinimizedControls()),
|
isVertical
|
||||||
PlayerMinimizedProgress(),
|
? const PlayerMinimizedControls()
|
||||||
|
: const PlayerMinimizedControlsDesktop(),
|
||||||
|
const PlayerMinimizedProgress(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -130,10 +136,7 @@ class PlayerMinimizedControls extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
|
|
||||||
// play/pause button
|
// play/pause button
|
||||||
Padding(
|
const AudiobookPlayerPlayPauseButton(),
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: AudiobookPlayerPlayPauseButton(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -162,3 +165,99 @@ class PlayerMinimizedProgress extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlayerMinimizedControlsDesktop extends HookConsumerWidget {
|
||||||
|
const PlayerMinimizedControlsDesktop({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentBook = ref.watch(currentBookProvider);
|
||||||
|
final currentChapter = ref.watch(currentChapterProvider);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (GoRouterState.of(context).topRoute?.name != Routes.player.name) {
|
||||||
|
context.pushNamed(Routes.player.name);
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const AudiobookPlayerSeekChapterButton(isForward: false),
|
||||||
|
// play/pause button
|
||||||
|
const AudiobookPlayerPlayPauseButton(),
|
||||||
|
const AudiobookPlayerSeekChapterButton(isForward: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// image
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(AppElementSizes.paddingSmall),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// navigate to item page
|
||||||
|
if (currentBook != null) {
|
||||||
|
context.pushNamed(
|
||||||
|
Routes.libraryItem.name,
|
||||||
|
pathParameters: {
|
||||||
|
Routes.libraryItem.pathParamName!:
|
||||||
|
currentBook.libraryItemId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: playerMinimizedHeight,
|
||||||
|
),
|
||||||
|
child: BookCoverWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// author and title of the book
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: AppElementSizes.paddingRegular,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// AutoScrollText(
|
||||||
|
Text(
|
||||||
|
'${currentBook?.metadata.title ?? ''} - ${currentChapter?.title ?? ''}',
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
|
// velocity:
|
||||||
|
// const Velocity(pixelsPerSecond: Offset(16, 0)),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
currentBook?.metadata.asBookMetadataExpanded.authorName ??
|
||||||
|
'',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
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:just_audio/just_audio.dart';
|
||||||
import 'package:vaani/features/player/providers/abs_provider.dart'
|
import 'package:vaani/features/player/providers/abs_provider.dart'
|
||||||
hide PlayerState;
|
hide PlayerState;
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
|
||||||
|
|
||||||
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||||
const AudiobookPlayerPlayPauseButton({
|
const AudiobookPlayerPlayPauseButton({
|
||||||
|
|
@ -21,12 +21,12 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _getIcon(AbsPlayerState playerState, BuildContext context) {
|
Widget _getIcon(PlayerState playerState, BuildContext context) {
|
||||||
if (playerState.playing) {
|
if (playerState.playing) {
|
||||||
return Icon(size: iconSize, Icons.pause);
|
return Icon(size: iconSize, Icons.pause);
|
||||||
} else {
|
} else {
|
||||||
switch (playerState.processingState) {
|
switch (playerState.processingState) {
|
||||||
case AbsProcessingState.loading || AbsProcessingState.buffering:
|
case ProcessingState.loading || ProcessingState.buffering:
|
||||||
return CircularProgressIndicator();
|
return CircularProgressIndicator();
|
||||||
default:
|
default:
|
||||||
return Icon(size: iconSize, Icons.play_arrow);
|
return Icon(size: iconSize, Icons.play_arrow);
|
||||||
|
|
@ -34,13 +34,13 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _actionButtonPressed(AbsPlayerState playerState, WidgetRef ref) async {
|
void _actionButtonPressed(PlayerState playerState, WidgetRef ref) async {
|
||||||
final player = ref.read(absPlayerProvider);
|
final player = ref.read(absPlayerProvider);
|
||||||
if (playerState.playing) {
|
if (playerState.playing) {
|
||||||
await player.pause();
|
await player.pause();
|
||||||
} else {
|
} else {
|
||||||
switch (playerState.processingState) {
|
switch (playerState.processingState) {
|
||||||
case AbsProcessingState.completed:
|
case ProcessingState.completed:
|
||||||
await player.seekInBook(const Duration(seconds: 0));
|
await player.seekInBook(const Duration(seconds: 0));
|
||||||
await player.play();
|
await player.play();
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final book = ref.watch(currentBookProvider);
|
||||||
final player = ref.watch(absPlayerProvider);
|
final player = ref.watch(absPlayerProvider);
|
||||||
final currentChapter = ref.watch(currentChapterProvider);
|
final currentChapter = ref.watch(currentChapterProvider);
|
||||||
final position = useStream(
|
final position = useStream(
|
||||||
|
|
@ -36,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
|
||||||
final progress =
|
final progress =
|
||||||
currentChapterProgress ?? position.data ?? const Duration(seconds: 0);
|
currentChapterProgress ?? position.data ?? const Duration(seconds: 0);
|
||||||
final total = currentChapter == null
|
final total = currentChapter == null
|
||||||
? player.book?.duration ?? const Duration(seconds: 0)
|
? book?.duration ?? const Duration(seconds: 0)
|
||||||
: currentChapter.end - currentChapter.start;
|
: currentChapter.end - currentChapter.start;
|
||||||
return ProgressBar(
|
return ProgressBar(
|
||||||
progress: progress,
|
progress: progress,
|
||||||
|
|
@ -65,6 +66,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final book = ref.watch(currentBookProvider);
|
||||||
final player = ref.read(absPlayerProvider);
|
final player = ref.read(absPlayerProvider);
|
||||||
final position = useStream(
|
final position = useStream(
|
||||||
player.positionInBookStream,
|
player.positionInBookStream,
|
||||||
|
|
@ -75,7 +77,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
|
||||||
height: AppElementSizes.barHeightLarge,
|
height: AppElementSizes.barHeightLarge,
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: (position.data ?? const Duration(seconds: 0)).inSeconds /
|
value: (position.data ?? const Duration(seconds: 0)).inSeconds /
|
||||||
(player.book?.duration ?? const Duration(seconds: 0)).inSeconds,
|
(book?.duration ?? const Duration(seconds: 0)).inSeconds,
|
||||||
borderRadius: BorderRadiusGeometry.all(Radius.circular(10)),
|
borderRadius: BorderRadiusGeometry.all(Radius.circular(10)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import 'package:vaani/features/settings/app_settings_provider.dart'
|
||||||
import 'package:vaani/features/settings/models/app_settings.dart';
|
import 'package:vaani/features/settings/models/app_settings.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/features/player/core/abs_audio_player.dart';
|
|
||||||
import 'package:vibration/vibration.dart';
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
import 'shake_detector.dart' as core;
|
import 'shake_detector.dart' as core;
|
||||||
|
|
@ -33,22 +32,22 @@ class ShakeDetector extends _$ShakeDetector {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no book is loaded, shake detection should not be enabled
|
// if no book is loaded, shake detection should not be enabled
|
||||||
|
final book = ref.watch(currentBookProvider);
|
||||||
final player = ref.watch(absPlayerProvider);
|
final player = ref.watch(absPlayerProvider);
|
||||||
player.playerStateStream.listen((event) {
|
player.playerStateStream.listen((event) {
|
||||||
if (event.processingState == AbsProcessingState.idle && wasPlayerLoaded) {
|
if (event.processingState == ProcessingState.idle && wasPlayerLoaded) {
|
||||||
_logger.config('Player is now not loaded, invalidating');
|
_logger.config('Player is now not loaded, invalidating');
|
||||||
wasPlayerLoaded = false;
|
wasPlayerLoaded = false;
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
if (event.processingState != AbsProcessingState.idle &&
|
if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) {
|
||||||
!wasPlayerLoaded) {
|
|
||||||
_logger.config('Player is now loaded, invalidating');
|
_logger.config('Player is now loaded, invalidating');
|
||||||
wasPlayerLoaded = true;
|
wasPlayerLoaded = true;
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (player.book == null) {
|
if (book == null) {
|
||||||
_logger.config('No book is loaded, disabling shake detection');
|
_logger.config('No book is loaded, disabling shake detection');
|
||||||
wasPlayerLoaded = false;
|
wasPlayerLoaded = false;
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -89,8 +88,10 @@ class ShakeDetector extends _$ShakeDetector {
|
||||||
ShakeAction shakeAction, {
|
ShakeAction shakeAction, {
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) {
|
}) {
|
||||||
|
final book = ref.read(currentBookProvider);
|
||||||
|
|
||||||
final player = ref.read(absPlayerProvider);
|
final player = ref.read(absPlayerProvider);
|
||||||
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
|
if (book == null && shakeAction.isPlaybackManagementEnabled) {
|
||||||
_logger.warning('No book is loaded');
|
_logger.warning('No book is loaded');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +123,7 @@ class ShakeDetector extends _$ShakeDetector {
|
||||||
return true;
|
return true;
|
||||||
case ShakeAction.playPause:
|
case ShakeAction.playPause:
|
||||||
_logger.fine('Toggling play/pause');
|
_logger.fine('Toggling play/pause');
|
||||||
player.playOrPause();
|
player.playing ? player.pause() : player.play();
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'shake_detector_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$shakeDetectorHash() => r'8e65e89d59a9cf9492fd5f3eb309eb3a37cf1c6d';
|
String _$shakeDetectorHash() => r'c2e6b6b2edf3a40a7a8f5a274f881911be68a5a0';
|
||||||
|
|
||||||
/// See also [ShakeDetector].
|
/// See also [ShakeDetector].
|
||||||
@ProviderFor(ShakeDetector)
|
@ProviderFor(ShakeDetector)
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,55 @@
|
||||||
import 'dart:async';
|
// import 'dart:async';
|
||||||
|
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
// import 'package:vaani/features/player/core/abs_audio_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';
|
||||||
|
|
||||||
class SkipStartEnd {
|
// class SkipStartEnd {
|
||||||
final Duration start;
|
// final Duration start;
|
||||||
final Duration end;
|
// final Duration end;
|
||||||
final AbsAudioPlayer player;
|
// final AbsAudioPlayer player;
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
// final List<StreamSubscription> _subscriptions = [];
|
||||||
final throttlerStart = Throttler(delay: Duration(seconds: 3));
|
// final throttlerStart = Throttler(delay: Duration(seconds: 3));
|
||||||
final throttlerEnd = Throttler(delay: Duration(seconds: 3));
|
// final throttlerEnd = Throttler(delay: Duration(seconds: 3));
|
||||||
|
|
||||||
SkipStartEnd({
|
// SkipStartEnd({
|
||||||
required this.start,
|
// required this.start,
|
||||||
required this.end,
|
// required this.end,
|
||||||
required this.player,
|
// required this.player,
|
||||||
}) {
|
// }) {
|
||||||
if (start > Duration.zero) {
|
// if (start > Duration.zero) {
|
||||||
_subscriptions.add(
|
// _subscriptions.add(
|
||||||
player.chapterStream.listen((chapter) async {
|
// player.chapterStream.listen((chapter) async {
|
||||||
if (chapter != null &&
|
// if (chapter != null &&
|
||||||
player.positionInChapter < Duration(seconds: 1)) {
|
// player.positionInChapter < Duration(seconds: 1)) {
|
||||||
player.seekInBook(chapter.start + start);
|
// player.seekInBook(chapter.start + start);
|
||||||
}
|
// }
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
if (end > Duration.zero) {
|
// if (end > Duration.zero) {
|
||||||
_subscriptions.add(
|
// _subscriptions.add(
|
||||||
player.positionInChapterStream.listen((positionChapter) {
|
// player.positionInChapterStream.listen((positionChapter) {
|
||||||
if (end >
|
// if (end >
|
||||||
(player.currentChapter?.duration ?? Duration.zero) -
|
// (player.currentChapter?.duration ?? Duration.zero) -
|
||||||
positionChapter) {
|
// positionChapter) {
|
||||||
Future.microtask(
|
// Future.microtask(
|
||||||
() => throttlerEnd.call(() => player.next()),
|
// () => throttlerEnd.call(() => player.next()),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// dispose the timer
|
// /// dispose the timer
|
||||||
void dispose() {
|
// void dispose() {
|
||||||
for (var sub in _subscriptions) {
|
// for (var sub in _subscriptions) {
|
||||||
sub.cancel();
|
// sub.cancel();
|
||||||
}
|
// }
|
||||||
throttlerStart.dispose();
|
// throttlerStart.dispose();
|
||||||
throttlerEnd.dispose();
|
// throttlerEnd.dispose();
|
||||||
// _playbackController.close();
|
// // _playbackController.close();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
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/abs_provider.dart';
|
// import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||||
import 'package:vaani/features/skip_start_end/core/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';
|
||||||
|
|
||||||
@riverpod
|
// @riverpod
|
||||||
class SkipStartEnd extends _$SkipStartEnd {
|
// class SkipStartEnd extends _$SkipStartEnd {
|
||||||
@override
|
// @override
|
||||||
core.SkipStartEnd? build() {
|
// core.SkipStartEnd? build() {
|
||||||
final currentBook = ref.watch(currentBookProvider);
|
// final currentBook = ref.watch(currentBookProvider);
|
||||||
final bookId = currentBook?.libraryItemId;
|
// final bookId = currentBook?.libraryItemId;
|
||||||
if (currentBook == null || bookId == null) {
|
// if (currentBook == null || bookId == null) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
final player = ref.read(absPlayerProvider);
|
// final player = ref.read(absPlayerProvider);
|
||||||
final bookSettings = ref.watch(bookSettingsProvider(bookId));
|
// final bookSettings = ref.watch(bookSettingsProvider(bookId));
|
||||||
final start = bookSettings.playerSettings.skipChapterStart;
|
// final start = bookSettings.playerSettings.skipChapterStart;
|
||||||
final end = bookSettings.playerSettings.skipChapterEnd;
|
// final end = bookSettings.playerSettings.skipChapterEnd;
|
||||||
if (start < Duration.zero && end < Duration.zero) {
|
// if (start < Duration.zero && end < Duration.zero) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
final skipStartEnd = core.SkipStartEnd(
|
// final skipStartEnd = core.SkipStartEnd(
|
||||||
start: start,
|
// start: start,
|
||||||
end: end,
|
// end: end,
|
||||||
player: player,
|
// player: player,
|
||||||
);
|
// );
|
||||||
ref.onDispose(skipStartEnd.dispose);
|
// ref.onDispose(skipStartEnd.dispose);
|
||||||
return skipStartEnd;
|
// return skipStartEnd;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'skip_start_end_provider.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$skipStartEndHash() => r'45572f40a098f081181e8b8bf9e4913e6e649cdc';
|
|
||||||
|
|
||||||
/// See also [SkipStartEnd].
|
|
||||||
@ProviderFor(SkipStartEnd)
|
|
||||||
final skipStartEndProvider =
|
|
||||||
AutoDisposeNotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
|
|
||||||
SkipStartEnd.new,
|
|
||||||
name: r'skipStartEndProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$SkipStartEnd = AutoDisposeNotifier<core.SkipStartEnd?>;
|
|
||||||
// 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,8 +1,8 @@
|
||||||
import 'dart:async';
|
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:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
|
||||||
|
|
||||||
/// this timer pauses the music player after a certain duration
|
/// this timer pauses the music player after a certain duration
|
||||||
///
|
///
|
||||||
|
|
@ -32,7 +32,7 @@ class SleepTimer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The player to be paused
|
/// The player to be paused
|
||||||
final AbsAudioPlayer player;
|
final AudioPlayer player;
|
||||||
|
|
||||||
/// The timer that will pause the player
|
/// The timer that will pause the player
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
|
|
@ -50,8 +50,8 @@ class SleepTimer {
|
||||||
SleepTimer({required duration, required this.player}) : _duration = duration {
|
SleepTimer({required duration, required this.player}) : _duration = duration {
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.playerStateStream.listen((event) {
|
player.playerStateStream.listen((event) {
|
||||||
if (event.processingState == AbsProcessingState.completed ||
|
if (event.processingState == ProcessingState.completed ||
|
||||||
event.processingState == AbsProcessingState.idle) {
|
event.processingState == ProcessingState.idle) {
|
||||||
clearCountDownTimer();
|
clearCountDownTimer();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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(absPlayerProvider),
|
player: ref.watch(simpleAudioPlayerProvider),
|
||||||
);
|
);
|
||||||
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(absPlayerProvider),
|
player: ref.watch(simpleAudioPlayerProvider),
|
||||||
);
|
);
|
||||||
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'417759e07a45e69af93bd9a1c78ac859d9abcf4b';
|
String _$sleepTimerHash() => r'7cac4509d8bd40c4d418c295d5b37c66492e7de9';
|
||||||
|
|
||||||
/// See also [SleepTimer].
|
/// See also [SleepTimer].
|
||||||
@ProviderFor(SleepTimer)
|
@ProviderFor(SleepTimer)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/server_provider.dart';
|
import 'package:vaani/api/server_provider.dart';
|
||||||
import 'package:vaani/db/storage.dart';
|
import 'package:vaani/db/storage.dart';
|
||||||
import 'package:vaani/features/logging/core/logger.dart';
|
import 'package:vaani/features/logging/core/logger.dart';
|
||||||
|
import 'package:vaani/features/player/core/init.dart';
|
||||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||||
import 'package:vaani/features/settings/api_settings_provider.dart';
|
import 'package:vaani/features/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||||
|
|
@ -36,8 +37,8 @@ void main() async {
|
||||||
await initStorage();
|
await initStorage();
|
||||||
|
|
||||||
// initialize audio player
|
// initialize audio player
|
||||||
// await configurePlayer();
|
await configurePlayer();
|
||||||
await container.read(configurePlayerProvider.future);
|
// await container.read(configurePlayerProvider.future);
|
||||||
// run the app
|
// run the app
|
||||||
runApp(
|
runApp(
|
||||||
UncontrolledProviderScope(
|
UncontrolledProviderScope(
|
||||||
|
|
@ -148,10 +149,12 @@ class AbsApp extends ConsumerWidget {
|
||||||
final appThemeLight = ThemeData(
|
final appThemeLight = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: lightColorScheme.harmonized(),
|
colorScheme: lightColorScheme.harmonized(),
|
||||||
|
fontFamily: fontFamilyPlatform,
|
||||||
);
|
);
|
||||||
final appThemeDark = ThemeData(
|
final appThemeDark = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: darkColorScheme.harmonized(),
|
colorScheme: darkColorScheme.harmonized(),
|
||||||
|
fontFamily: fontFamilyPlatform,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
// TODO bottom sheet theme is not working
|
// TODO bottom sheet theme is not working
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ class PlayerPage extends HookConsumerWidget {
|
||||||
final isVertical = size.height > size.width;
|
final isVertical = size.height > size.width;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
// 以下两项确保在滚动后背景色不变
|
||||||
|
// elevation: 0 是保持 AppBar 不变的关键
|
||||||
|
elevation: 0,
|
||||||
|
// 设置 forceMaterialTransparency 防止滚动时的透明度变化
|
||||||
|
forceMaterialTransparency: true,
|
||||||
title: Text(currentBook.metadata.title ?? ''),
|
title: Text(currentBook.metadata.title ?? ''),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// brand color rgb(49, 27, 146) rgb(96, 76, 236)
|
// brand color rgb(49, 27, 146) rgb(96, 76, 236)
|
||||||
|
|
@ -13,3 +15,18 @@ final brandDarkColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: brandColor,
|
seedColor: brandColor,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// 系统字体(跨平台)
|
||||||
|
String get fontFamilyPlatform {
|
||||||
|
if (Platform.isIOS || Platform.isMacOS) {
|
||||||
|
return 'PingFang SC'; // 苹方,仅苹果设备
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
return 'Roboto'; // Android 默认
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
return 'Microsoft YaHei'; // Windows 微软雅黑
|
||||||
|
// } else if (Platform.isLinux) {
|
||||||
|
// return 'Ubuntu'; // Linux
|
||||||
|
} else {
|
||||||
|
return 'Arial'; // 其他平台回退
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -770,6 +770,15 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.5"
|
version: "0.10.5"
|
||||||
|
just_audio_background:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: just_audio_background
|
||||||
|
ref: media-notification-config
|
||||||
|
resolved-ref: fce45f334f0838cb6f630548efb65fec40ff17b4
|
||||||
|
url: "https://github.com/Dr-Blank/just_audio"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1-beta.15"
|
||||||
just_audio_media_kit:
|
just_audio_media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
13
pubspec.yaml
13
pubspec.yaml
|
|
@ -87,14 +87,15 @@ dependencies:
|
||||||
|
|
||||||
# 音频播放
|
# 音频播放
|
||||||
audio_service: ^0.18.15
|
audio_service: ^0.18.15
|
||||||
|
# audio_service_win: ^0.0.2
|
||||||
audio_session: ^0.1.23
|
audio_session: ^0.1.23
|
||||||
just_audio: ^0.10.5
|
just_audio: ^0.10.5
|
||||||
# just_audio_background:
|
just_audio_background:
|
||||||
# # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed
|
# TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed
|
||||||
# git:
|
git:
|
||||||
# url: https://github.com/Dr-Blank/just_audio
|
url: https://github.com/Dr-Blank/just_audio
|
||||||
# ref: media-notification-config
|
ref: media-notification-config
|
||||||
# path: just_audio_background
|
path: just_audio_background
|
||||||
# just_audio_windows: ^0.2.2
|
# just_audio_windows: ^0.2.2
|
||||||
just_audio_media_kit: ^2.0.4
|
just_audio_media_kit: ^2.0.4
|
||||||
media_kit_libs_linux: any
|
media_kit_libs_linux: any
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue