mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-15 22:09:35 +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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final book = ref.watch(currentBookProvider);
|
||||
final player = ref.watch(absPlayerProvider);
|
||||
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull;
|
||||
if (libraryItem == null) {
|
||||
|
|
@ -146,13 +147,13 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
final mediaProgress = libraryItem.userMediaProgress;
|
||||
if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) {
|
||||
if (mediaProgress == null && book?.libraryItemId != libraryItem.id) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
double progress;
|
||||
Duration remainingTime;
|
||||
if (player.book?.libraryItemId == libraryItem.id) {
|
||||
if (book?.libraryItemId == libraryItem.id) {
|
||||
// final positionStream = useStream(player.slowPositionStream);
|
||||
progress = (player.positionInBook).inSeconds /
|
||||
libraryItem.media.asBookExpanded.duration.inSeconds;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:logging/logging.dart';
|
|||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||
import 'package:vaani/shared/utils/error_response.dart';
|
||||
|
||||
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 'package:audio_service/audio_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:just_audio_background/just_audio_background.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
|
|
@ -16,20 +17,32 @@ final offset = Duration(milliseconds: 10);
|
|||
|
||||
final _logger = Logger('AbsAudioPlayer');
|
||||
|
||||
/// 音频播放器抽象类
|
||||
abstract class AbsAudioPlayer {
|
||||
final _mediaItemController = BehaviorSubject<MediaItem?>.seeded(null);
|
||||
final playerStateSubject =
|
||||
BehaviorSubject.seeded(AbsPlayerState(false, AbsProcessingState.idle));
|
||||
class AbsAudioPlayer {
|
||||
late final AudioPlayer _player;
|
||||
AbsAudioPlayer(AudioPlayer player) : _player = player {
|
||||
_player.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final _bookStreamController = BehaviorSubject<BookExpanded?>.seeded(null);
|
||||
final chapterStreamController = BehaviorSubject<BookChapter?>.seeded(null);
|
||||
|
||||
BookExpanded? get book => _bookStreamController.nvalue;
|
||||
AudioTrack? get currentTrack => book?.tracks[currentIndex];
|
||||
BookChapter? get currentChapter => chapterStreamController.nvalue;
|
||||
AbsPlayerState get playerState => playerStateSubject.value;
|
||||
Stream<MediaItem?> get mediaItemStream => _mediaItemController.stream;
|
||||
Stream<AbsPlayerState> get playerStateStream => playerStateSubject.stream;
|
||||
PlayerState get playerState => _player.playerState;
|
||||
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
|
||||
|
||||
// 加载整本书
|
||||
Future<void> load(
|
||||
|
|
@ -59,43 +72,77 @@ abstract class AbsAudioPlayer {
|
|||
.formatNotificationTitle(book);
|
||||
chapterStreamController
|
||||
.add(book.findChapterAtTime(initialPosition ?? Duration.zero));
|
||||
final item = MediaItem(
|
||||
id: book.libraryItemId,
|
||||
title: title,
|
||||
artist: artist,
|
||||
duration: currentChapter?.duration ?? book.duration,
|
||||
artUri: Uri.parse(
|
||||
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
||||
),
|
||||
);
|
||||
_mediaItemController.sink.add(item);
|
||||
final playlist = book.tracks
|
||||
.map(
|
||||
(track) => (
|
||||
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
|
||||
track.duration
|
||||
// final item = MediaItem(
|
||||
// id: book.libraryItemId,
|
||||
// title: title,
|
||||
// artist: artist,
|
||||
// duration: currentChapter?.duration ?? book.duration,
|
||||
// artUri: Uri.parse(
|
||||
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
||||
// ),
|
||||
// );
|
||||
|
||||
mediaItem(track) => MediaItem(
|
||||
id: book.libraryItemId + track.index.toString(),
|
||||
title: title,
|
||||
artist: artist,
|
||||
duration: currentChapter?.duration ?? book.duration,
|
||||
artUri: Uri.parse(
|
||||
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
await setPlayList(
|
||||
playlist,
|
||||
index: indexTrack,
|
||||
position: positionInTrack,
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
);
|
||||
List<AudioSource> audioSources = start != null && start > Duration.zero ||
|
||||
end != null && end > Duration.zero
|
||||
? book.tracks
|
||||
.map(
|
||||
(track) => ClippingAudioSource(
|
||||
child: AudioSource.uri(
|
||||
_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(
|
||||
List<(Uri, Duration)> playlist, {
|
||||
int? index,
|
||||
Duration? position,
|
||||
Duration? start,
|
||||
Duration? end,
|
||||
});
|
||||
Future<void> play();
|
||||
Future<void> pause();
|
||||
Future<void> playOrPause();
|
||||
Future<void> play() async {
|
||||
await _player.play();
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
await _player.pause();
|
||||
}
|
||||
|
||||
Future<void> playOrPause() async {
|
||||
_player.playing ? await _player.pause() : await _player.play();
|
||||
}
|
||||
|
||||
// 跳到下一章
|
||||
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 {
|
||||
if (book == null) return;
|
||||
// 找到目标位置所在音轨和音轨内的位置
|
||||
|
|
@ -140,8 +199,6 @@ abstract class AbsAudioPlayer {
|
|||
await seek(positionInTrack, index: index);
|
||||
}
|
||||
|
||||
Future<void> setSpeed(double speed);
|
||||
Future<void> setVolume(double volume);
|
||||
Future<void> switchChapter(int chapterId) async {
|
||||
if (book == null) return;
|
||||
|
||||
|
|
@ -153,15 +210,18 @@ abstract class AbsAudioPlayer {
|
|||
}
|
||||
|
||||
bool get playing => playerState.playing;
|
||||
Stream<bool> get playingStream;
|
||||
Stream<bool> get playingStream => _player.playingStream;
|
||||
Stream<BookExpanded?> get bookStream => _bookStreamController.stream;
|
||||
Stream<BookChapter?> get chapterStream => chapterStreamController.stream;
|
||||
|
||||
int get currentIndex;
|
||||
double get speed;
|
||||
int get currentIndex => _player.currentIndex ?? 0;
|
||||
double get speed => _player.speed;
|
||||
|
||||
Duration get position;
|
||||
Stream<Duration> get positionStream;
|
||||
Duration get position => _addClippingStart(_player.position);
|
||||
Stream<Duration> get positionStream =>
|
||||
_player.positionStream.where((_) => _player.playing).map((position) {
|
||||
return _addClippingStart(position);
|
||||
});
|
||||
|
||||
Duration get positionInChapter => getPositionInChapter(position);
|
||||
Duration getPositionInChapter(position) {
|
||||
|
|
@ -183,8 +243,8 @@ abstract class AbsAudioPlayer {
|
|||
return positionInBook;
|
||||
});
|
||||
|
||||
Duration get bufferedPosition;
|
||||
Stream<Duration> get bufferedPositionStream;
|
||||
Duration get bufferedPosition => _player.bufferedPosition;
|
||||
Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream;
|
||||
Duration get bufferedPositionInBook =>
|
||||
bufferedPosition +
|
||||
(book?.tracks[currentIndex].startOffset ?? Duration.zero);
|
||||
|
|
@ -193,70 +253,26 @@ abstract class AbsAudioPlayer {
|
|||
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() {
|
||||
_mediaItemController.close();
|
||||
playerStateSubject.close();
|
||||
_bookStreamController.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(
|
||||
AudioTrack track,
|
||||
List<Uri>? downloadedUris, {
|
||||
|
|
@ -280,6 +296,38 @@ extension _ValueStreamExtension<T> on ValueStream<T> {
|
|||
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 {
|
||||
String formatNotificationTitle(BookExpanded book) {
|
||||
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_session/audio_session.dart';
|
||||
// import 'package:just_audio_background/just_audio_background.dart'
|
||||
// show JustAudioBackground, NotificationConfig;
|
||||
// import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
||||
// show JustAudioMediaKit;
|
||||
// import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
// import 'package:vaani/features/settings/models/app_settings.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:just_audio_background/just_audio_background.dart'
|
||||
show JustAudioBackground, NotificationConfig;
|
||||
import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
||||
show JustAudioMediaKit;
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/models/app_settings.dart';
|
||||
|
||||
// Future<void> configurePlayer() async {
|
||||
// // for playing audio on windows, linux
|
||||
// JustAudioMediaKit.ensureInitialized();
|
||||
Future<void> configurePlayer() async {
|
||||
// for playing audio on windows, linux
|
||||
JustAudioMediaKit.ensureInitialized();
|
||||
|
||||
// // for configuring how this app will interact with other audio apps
|
||||
// final session = await AudioSession.instance;
|
||||
// await session.configure(const AudioSessionConfiguration.speech());
|
||||
// 跳转到播放列表指定条目指定位置
|
||||
// prefetch-playlist=yes
|
||||
// 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
|
||||
// await JustAudioBackground.init(
|
||||
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||
// androidNotificationChannelName: 'Audio playback',
|
||||
// androidNotificationOngoing: false,
|
||||
// androidStopForegroundOnPause: false,
|
||||
// androidNotificationChannelDescription: 'Audio playback in the background',
|
||||
// androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||
// rewindInterval: appSettings.notificationSettings.rewindInterval,
|
||||
// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
|
||||
// androidShowNotificationBadge: false,
|
||||
// notificationConfigBuilder: (state) {
|
||||
// final controls = [
|
||||
// if (appSettings.notificationSettings.mediaControls
|
||||
// .contains(NotificationMediaControl.skipToPreviousChapter) &&
|
||||
// state.hasPrevious)
|
||||
// MediaControl.skipToPrevious,
|
||||
// if (appSettings.notificationSettings.mediaControls
|
||||
// .contains(NotificationMediaControl.rewind))
|
||||
// MediaControl.rewind,
|
||||
// if (state.playing) MediaControl.pause else MediaControl.play,
|
||||
// if (appSettings.notificationSettings.mediaControls
|
||||
// .contains(NotificationMediaControl.fastForward))
|
||||
// MediaControl.fastForward,
|
||||
// if (appSettings.notificationSettings.mediaControls
|
||||
// .contains(NotificationMediaControl.skipToNextChapter) &&
|
||||
// state.hasNext)
|
||||
// MediaControl.skipToNext,
|
||||
// if (appSettings.notificationSettings.mediaControls
|
||||
// .contains(NotificationMediaControl.stop))
|
||||
// MediaControl.stop,
|
||||
// ];
|
||||
// return NotificationConfig(
|
||||
// controls: controls,
|
||||
// systemActions: const {
|
||||
// MediaAction.seek,
|
||||
// MediaAction.seekForward,
|
||||
// MediaAction.seekBackward,
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
final appSettings = loadOrCreateAppSettings();
|
||||
|
||||
// for playing audio in the background
|
||||
await JustAudioBackground.init(
|
||||
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||
androidNotificationChannelName: 'Audio playback',
|
||||
androidNotificationOngoing: false,
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationChannelDescription: 'Audio playback in the background',
|
||||
androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||
rewindInterval: appSettings.notificationSettings.rewindInterval,
|
||||
fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
|
||||
androidShowNotificationBadge: false,
|
||||
notificationConfigBuilder: (state) {
|
||||
final controls = [
|
||||
if (appSettings.notificationSettings.mediaControls
|
||||
.contains(NotificationMediaControl.skipToPreviousChapter) &&
|
||||
state.hasPrevious)
|
||||
MediaControl.skipToPrevious,
|
||||
if (appSettings.notificationSettings.mediaControls
|
||||
.contains(NotificationMediaControl.rewind))
|
||||
MediaControl.rewind,
|
||||
if (state.playing) MediaControl.pause else MediaControl.play,
|
||||
if (appSettings.notificationSettings.mediaControls
|
||||
.contains(NotificationMediaControl.fastForward))
|
||||
MediaControl.fastForward,
|
||||
if (appSettings.notificationSettings.mediaControls
|
||||
.contains(NotificationMediaControl.skipToNextChapter) &&
|
||||
state.hasNext)
|
||||
MediaControl.skipToNext,
|
||||
if (appSettings.notificationSettings.mediaControls
|
||||
.contains(NotificationMediaControl.stop))
|
||||
MediaControl.stop,
|
||||
];
|
||||
return NotificationConfig(
|
||||
controls: controls,
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
MediaAction.seekForward,
|
||||
MediaAction.seekBackward,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:just_audio/just_audio.dart' as audio;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart' as api;
|
||||
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/features/downloads/providers/download_manager.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' as core;
|
||||
import 'package:vaani/features/player/core/abs_audio_player_platform.dart';
|
||||
import 'package:vaani/features/player/core/abs_audio_player.dart'
|
||||
show AbsAudioPlayer;
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/shared/extensions/box.dart';
|
||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||
|
||||
part 'abs_provider.g.dart';
|
||||
|
||||
final _logger = Logger('AbsPlayerProvider');
|
||||
|
||||
/// 音频播放器 配置
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<AudioHandler> configurePlayer(Ref ref) async {
|
||||
final player = ref.read(absPlayerProvider);
|
||||
// for playing audio on windows, linux
|
||||
// @Riverpod(keepAlive: true)
|
||||
// Future<AudioHandler> configurePlayer(Ref ref) async {
|
||||
// final player = ref.read(absPlayerProvider);
|
||||
// // for playing audio on windows, linux
|
||||
|
||||
// for configuring how this app will interact with other audio apps
|
||||
final session = await AudioSession.instance;
|
||||
await session.configure(const AudioSessionConfiguration.speech());
|
||||
// // for configuring how this app will interact with other audio apps
|
||||
// final session = await AudioSession.instance;
|
||||
// await session.configure(const AudioSessionConfiguration.speech());
|
||||
|
||||
final audioService = await AudioService.init(
|
||||
builder: () => AbsAudioHandler(player),
|
||||
config: const AudioServiceConfig(
|
||||
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||
androidNotificationChannelName: 'ABSPlayback',
|
||||
androidNotificationChannelDescription:
|
||||
'Needed to control audio from lock screen',
|
||||
androidNotificationOngoing: false,
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||
preloadArtwork: true,
|
||||
// fastForwardInterval: Duration(seconds: 20),
|
||||
// rewindInterval: Duration(seconds: 20),
|
||||
),
|
||||
);
|
||||
// final audioService = await AudioService.init(
|
||||
// builder: () => AbsAudioHandler(player),
|
||||
// config: const AudioServiceConfig(
|
||||
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
|
||||
// androidNotificationChannelName: 'ABSPlayback',
|
||||
// androidNotificationChannelDescription:
|
||||
// 'Needed to control audio from lock screen',
|
||||
// androidNotificationOngoing: false,
|
||||
// androidStopForegroundOnPause: false,
|
||||
// androidNotificationIcon: 'drawable/ic_stat_logo',
|
||||
// preloadArtwork: true,
|
||||
// // fastForwardInterval: Duration(seconds: 20),
|
||||
// // rewindInterval: Duration(seconds: 20),
|
||||
// ),
|
||||
// );
|
||||
|
||||
_logger.finer('created simple player');
|
||||
return audioService;
|
||||
}
|
||||
// _logger.finer('created simple player');
|
||||
// return audioService;
|
||||
// }
|
||||
|
||||
// just_audio 播放器
|
||||
@Riverpod(keepAlive: true)
|
||||
core.AbsAudioPlayer audioPlayer(Ref ref) {
|
||||
final player = AbsPlatformAudioPlayer();
|
||||
// final player = AbsMpvAudioPlayer();
|
||||
ref.onDispose(player.dispose);
|
||||
return player;
|
||||
}
|
||||
// @Riverpod(keepAlive: true)
|
||||
// core.AbsAudioPlayer audioPlayer(Ref ref) {
|
||||
// final player = AbsPlatformAudioPlayer();
|
||||
// // final player = AbsMpvAudioPlayer();
|
||||
// ref.onDispose(player.dispose);
|
||||
// return player;
|
||||
// }
|
||||
|
||||
// 播放器激活状态
|
||||
@riverpod
|
||||
|
|
@ -69,129 +63,20 @@ bool playerActive(Ref ref) {
|
|||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AudioPlayer simpleAudioPlayer(Ref ref) {
|
||||
final player = AudioPlayer();
|
||||
audio.AudioPlayer simpleAudioPlayer(Ref ref) {
|
||||
final player = audio.AudioPlayer();
|
||||
ref.onDispose(player.dispose);
|
||||
return player;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AbsAudioPlayer extends _$AbsAudioPlayer {
|
||||
@override
|
||||
AudioPlayer build() {
|
||||
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
|
||||
return audioPlayer;
|
||||
}
|
||||
final offset = Duration(milliseconds: 10);
|
||||
|
||||
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)
|
||||
class AbsPlayer extends _$AbsPlayer {
|
||||
@override
|
||||
core.AbsAudioPlayer build() {
|
||||
final audioPlayer = ref.watch(audioPlayerProvider);
|
||||
return audioPlayer;
|
||||
AbsAudioPlayer build() {
|
||||
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
|
||||
return AbsAudioPlayer(audioPlayer);
|
||||
}
|
||||
|
||||
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
|
||||
class PlayerState extends _$PlayerState {
|
||||
@override
|
||||
core.AbsPlayerState build() {
|
||||
audio.PlayerState build() {
|
||||
final player = ref.read(absPlayerProvider);
|
||||
player.playerStateStream.listen((playerState) {
|
||||
if (playerState != state) {
|
||||
|
|
@ -260,10 +206,10 @@ class PlayerState extends _$PlayerState {
|
|||
}
|
||||
|
||||
bool isLoading(String itemId) {
|
||||
final player = ref.read(absPlayerProvider);
|
||||
return player.book?.libraryItemId == itemId &&
|
||||
final book = ref.read(currentBookProvider);
|
||||
return book?.libraryItemId == itemId &&
|
||||
!state.playing &&
|
||||
state.processingState == core.AbsProcessingState.loading;
|
||||
state.processingState == audio.ProcessingState.loading;
|
||||
}
|
||||
|
||||
bool isPlaying() {
|
||||
|
|
@ -307,7 +253,7 @@ class CurrentBook extends _$CurrentBook {
|
|||
|
||||
Future<void> update(String libraryItemId, {bool play = true}) async {
|
||||
if (state?.libraryItemId == libraryItemId) {
|
||||
ref.read(audioPlayerProvider).playOrPause();
|
||||
ref.read(absPlayerProvider).playOrPause();
|
||||
return;
|
||||
}
|
||||
final book = await ref.read(libraryItemProvider(libraryItemId).future);
|
||||
|
|
|
|||
|
|
@ -6,44 +6,11 @@ part of 'abs_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$configurePlayerHash() => r'7ac63b6c3a34c56f42be55bc7a4856dabaae1583';
|
||||
String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910';
|
||||
|
||||
/// 音频播放器 配置
|
||||
///
|
||||
/// Copied from [configurePlayer].
|
||||
@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].
|
||||
/// Copied from [playerActive].
|
||||
@ProviderFor(playerActive)
|
||||
final playerActiveProvider = AutoDisposeProvider<bool>.internal(
|
||||
playerActive,
|
||||
|
|
@ -57,11 +24,11 @@ final playerActiveProvider = AutoDisposeProvider<bool>.internal(
|
|||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
|
||||
String _$simpleAudioPlayerHash() => r'4da667e3b7047003edd594f8a76700afb963aceb';
|
||||
String _$simpleAudioPlayerHash() => r'99d84a750cf605ad036603320925f0ba7253930b';
|
||||
|
||||
/// See also [simpleAudioPlayer].
|
||||
@ProviderFor(simpleAudioPlayer)
|
||||
final simpleAudioPlayerProvider = Provider<AudioPlayer>.internal(
|
||||
final simpleAudioPlayerProvider = Provider<audio.AudioPlayer>.internal(
|
||||
simpleAudioPlayer,
|
||||
name: r'simpleAudioPlayerProvider',
|
||||
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')
|
||||
// ignore: unused_element
|
||||
typedef SimpleAudioPlayerRef = ProviderRef<AudioPlayer>;
|
||||
typedef SimpleAudioPlayerRef = ProviderRef<audio.AudioPlayer>;
|
||||
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
|
|
@ -242,30 +209,11 @@ final positionChapterProvider = AutoDisposeStreamProvider<Duration>.internal(
|
|||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PositionChapterRef = AutoDisposeStreamProviderRef<Duration>;
|
||||
String _$absAudioPlayerHash() => r'f595b5033eed9f4a4aa07c297c4a176955e6aab1';
|
||||
String _$absPlayerHash() => r'370f576d3d3a2196d1a93f2046005c1a3298d994';
|
||||
|
||||
/// See also [AbsAudioPlayer].
|
||||
@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].
|
||||
/// See also [AbsPlayer].
|
||||
@ProviderFor(AbsPlayer)
|
||||
final absPlayerProvider =
|
||||
NotifierProvider<AbsPlayer, core.AbsAudioPlayer>.internal(
|
||||
final absPlayerProvider = NotifierProvider<AbsPlayer, AbsAudioPlayer>.internal(
|
||||
AbsPlayer.new,
|
||||
name: r'absPlayerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
|
|
@ -274,13 +222,15 @@ final absPlayerProvider =
|
|||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AbsPlayer = Notifier<core.AbsAudioPlayer>;
|
||||
String _$playerStateHash() => r'f195d2d13bcee0f91b862e669ab3549667d8dd2d';
|
||||
typedef _$AbsPlayer = Notifier<AbsAudioPlayer>;
|
||||
String _$playerStateHash() => r'eb79bd816714f721da1c4226d4447de5dc55fc5c';
|
||||
|
||||
/// See also [PlayerState].
|
||||
/// 音频播放器 riverpod状态
|
||||
///
|
||||
/// Copied from [PlayerState].
|
||||
@ProviderFor(PlayerState)
|
||||
final playerStateProvider =
|
||||
AutoDisposeNotifierProvider<PlayerState, core.AbsPlayerState>.internal(
|
||||
AutoDisposeNotifierProvider<PlayerState, audio.PlayerState>.internal(
|
||||
PlayerState.new,
|
||||
name: r'playerStateProvider',
|
||||
debugGetCreateSourceHash:
|
||||
|
|
@ -289,8 +239,8 @@ final playerStateProvider =
|
|||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
|
||||
String _$currentBookHash() => r'714d7701508b6186598e13bc38c57c3fe644ae90';
|
||||
typedef _$PlayerState = AutoDisposeNotifier<audio.PlayerState>;
|
||||
String _$currentBookHash() => r'85de9041d356e214761b65bd1b7b74321d5a9221';
|
||||
|
||||
/// See also [CurrentBook].
|
||||
@ProviderFor(CurrentBook)
|
||||
|
|
|
|||
|
|
@ -62,17 +62,17 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
|||
// add a shadow to the image elevation hovering effect
|
||||
child: PlayerExpandedImage(imageSize),
|
||||
),
|
||||
_buildControls(imageSize),
|
||||
SizedBox(
|
||||
width: imageSize,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: AppElementSizes.paddingRegular,
|
||||
right: AppElementSizes.paddingRegular,
|
||||
),
|
||||
child: const AudiobookChapterProgressBar(),
|
||||
),
|
||||
),
|
||||
// _buildControls(imageSize),
|
||||
// SizedBox(
|
||||
// width: imageSize,
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(
|
||||
// left: AppElementSizes.paddingRegular,
|
||||
// right: AppElementSizes.paddingRegular,
|
||||
// ),
|
||||
// child: const AudiobookChapterProgressBar(),
|
||||
// ),
|
||||
// ),
|
||||
_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),
|
||||
// buttonSkipBackwards
|
||||
const AudiobookPlayerSeekButton(isForward: false),
|
||||
AudiobookPlayerPlayPauseButton(),
|
||||
const AudiobookPlayerPlayPauseButton(),
|
||||
// // buttonSkipForwards
|
||||
const AudiobookPlayerSeekButton(isForward: true),
|
||||
// // next chapter
|
||||
|
|
@ -144,6 +174,8 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const AudiobookPlayerSeekButton(isForward: false),
|
||||
const AudiobookPlayerSeekButton(isForward: true),
|
||||
// speed control
|
||||
const PlayerSpeedAdjustButton(),
|
||||
const Spacer(),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/constants/sizes.dart';
|
||||
import 'package:vaani/features/player/providers/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/router/router.dart';
|
||||
import 'package:vaani/shared/extensions/chapter.dart';
|
||||
|
|
@ -22,6 +23,9 @@ class PlayerMinimized extends HookConsumerWidget {
|
|||
if (currentBook == null) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
final size = MediaQuery.of(context).size;
|
||||
// 竖屏
|
||||
final isVertical = size.height > size.width;
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
height: playerMinimizedHeight,
|
||||
|
|
@ -29,8 +33,10 @@ class PlayerMinimized extends HookConsumerWidget {
|
|||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Hero(tag: 'player_hero', child: const PlayerMinimizedControls()),
|
||||
PlayerMinimizedProgress(),
|
||||
isVertical
|
||||
? const PlayerMinimizedControls()
|
||||
: const PlayerMinimizedControlsDesktop(),
|
||||
const PlayerMinimizedProgress(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -130,10 +136,7 @@ class PlayerMinimizedControls extends HookConsumerWidget {
|
|||
),
|
||||
|
||||
// play/pause button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: AudiobookPlayerPlayPauseButton(),
|
||||
),
|
||||
const 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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:vaani/features/player/providers/abs_provider.dart'
|
||||
hide PlayerState;
|
||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
||||
|
||||
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
||||
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) {
|
||||
return Icon(size: iconSize, Icons.pause);
|
||||
} else {
|
||||
switch (playerState.processingState) {
|
||||
case AbsProcessingState.loading || AbsProcessingState.buffering:
|
||||
case ProcessingState.loading || ProcessingState.buffering:
|
||||
return CircularProgressIndicator();
|
||||
default:
|
||||
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);
|
||||
if (playerState.playing) {
|
||||
await player.pause();
|
||||
} else {
|
||||
switch (playerState.processingState) {
|
||||
case AbsProcessingState.completed:
|
||||
case ProcessingState.completed:
|
||||
await player.seekInBook(const Duration(seconds: 0));
|
||||
await player.play();
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final book = ref.watch(currentBookProvider);
|
||||
final player = ref.watch(absPlayerProvider);
|
||||
final currentChapter = ref.watch(currentChapterProvider);
|
||||
final position = useStream(
|
||||
|
|
@ -36,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
|
|||
final progress =
|
||||
currentChapterProgress ?? position.data ?? const Duration(seconds: 0);
|
||||
final total = currentChapter == null
|
||||
? player.book?.duration ?? const Duration(seconds: 0)
|
||||
? book?.duration ?? const Duration(seconds: 0)
|
||||
: currentChapter.end - currentChapter.start;
|
||||
return ProgressBar(
|
||||
progress: progress,
|
||||
|
|
@ -65,6 +66,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final book = ref.watch(currentBookProvider);
|
||||
final player = ref.read(absPlayerProvider);
|
||||
final position = useStream(
|
||||
player.positionInBookStream,
|
||||
|
|
@ -75,7 +77,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
|
|||
height: AppElementSizes.barHeightLarge,
|
||||
child: LinearProgressIndicator(
|
||||
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)),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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/sleep_timer/providers/sleep_timer_provider.dart'
|
||||
show sleepTimerProvider;
|
||||
import 'package:vaani/features/player/core/abs_audio_player.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
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
|
||||
final book = ref.watch(currentBookProvider);
|
||||
final player = ref.watch(absPlayerProvider);
|
||||
player.playerStateStream.listen((event) {
|
||||
if (event.processingState == AbsProcessingState.idle && wasPlayerLoaded) {
|
||||
if (event.processingState == ProcessingState.idle && wasPlayerLoaded) {
|
||||
_logger.config('Player is now not loaded, invalidating');
|
||||
wasPlayerLoaded = false;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
if (event.processingState != AbsProcessingState.idle &&
|
||||
!wasPlayerLoaded) {
|
||||
if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) {
|
||||
_logger.config('Player is now loaded, invalidating');
|
||||
wasPlayerLoaded = true;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
});
|
||||
|
||||
if (player.book == null) {
|
||||
if (book == null) {
|
||||
_logger.config('No book is loaded, disabling shake detection');
|
||||
wasPlayerLoaded = false;
|
||||
return null;
|
||||
|
|
@ -89,8 +88,10 @@ class ShakeDetector extends _$ShakeDetector {
|
|||
ShakeAction shakeAction, {
|
||||
required Ref ref,
|
||||
}) {
|
||||
final book = ref.read(currentBookProvider);
|
||||
|
||||
final player = ref.read(absPlayerProvider);
|
||||
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
|
||||
if (book == null && shakeAction.isPlaybackManagementEnabled) {
|
||||
_logger.warning('No book is loaded');
|
||||
return false;
|
||||
}
|
||||
|
|
@ -122,7 +123,7 @@ class ShakeDetector extends _$ShakeDetector {
|
|||
return true;
|
||||
case ShakeAction.playPause:
|
||||
_logger.fine('Toggling play/pause');
|
||||
player.playOrPause();
|
||||
player.playing ? player.pause() : player.play();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'shake_detector_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$shakeDetectorHash() => r'8e65e89d59a9cf9492fd5f3eb309eb3a37cf1c6d';
|
||||
String _$shakeDetectorHash() => r'c2e6b6b2edf3a40a7a8f5a274f881911be68a5a0';
|
||||
|
||||
/// See also [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/shared/extensions/chapter.dart';
|
||||
import 'package:vaani/shared/utils/throttler.dart';
|
||||
// import 'package:vaani/features/player/core/abs_audio_player.dart';
|
||||
// import 'package:vaani/shared/extensions/chapter.dart';
|
||||
// import 'package:vaani/shared/utils/throttler.dart';
|
||||
|
||||
class SkipStartEnd {
|
||||
final Duration start;
|
||||
final Duration end;
|
||||
final AbsAudioPlayer player;
|
||||
// class SkipStartEnd {
|
||||
// final Duration start;
|
||||
// final Duration end;
|
||||
// final AbsAudioPlayer player;
|
||||
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final throttlerStart = Throttler(delay: Duration(seconds: 3));
|
||||
final throttlerEnd = Throttler(delay: Duration(seconds: 3));
|
||||
// final List<StreamSubscription> _subscriptions = [];
|
||||
// final throttlerStart = Throttler(delay: Duration(seconds: 3));
|
||||
// final throttlerEnd = Throttler(delay: Duration(seconds: 3));
|
||||
|
||||
SkipStartEnd({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.player,
|
||||
}) {
|
||||
if (start > Duration.zero) {
|
||||
_subscriptions.add(
|
||||
player.chapterStream.listen((chapter) async {
|
||||
if (chapter != null &&
|
||||
player.positionInChapter < Duration(seconds: 1)) {
|
||||
player.seekInBook(chapter.start + start);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (end > Duration.zero) {
|
||||
_subscriptions.add(
|
||||
player.positionInChapterStream.listen((positionChapter) {
|
||||
if (end >
|
||||
(player.currentChapter?.duration ?? Duration.zero) -
|
||||
positionChapter) {
|
||||
Future.microtask(
|
||||
() => throttlerEnd.call(() => player.next()),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// SkipStartEnd({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.player,
|
||||
// }) {
|
||||
// if (start > Duration.zero) {
|
||||
// _subscriptions.add(
|
||||
// player.chapterStream.listen((chapter) async {
|
||||
// if (chapter != null &&
|
||||
// player.positionInChapter < Duration(seconds: 1)) {
|
||||
// player.seekInBook(chapter.start + start);
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// if (end > Duration.zero) {
|
||||
// _subscriptions.add(
|
||||
// player.positionInChapterStream.listen((positionChapter) {
|
||||
// if (end >
|
||||
// (player.currentChapter?.duration ?? Duration.zero) -
|
||||
// positionChapter) {
|
||||
// Future.microtask(
|
||||
// () => throttlerEnd.call(() => player.next()),
|
||||
// );
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
/// dispose the timer
|
||||
void dispose() {
|
||||
for (var sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
throttlerStart.dispose();
|
||||
throttlerEnd.dispose();
|
||||
// _playbackController.close();
|
||||
}
|
||||
}
|
||||
// /// dispose the timer
|
||||
// void dispose() {
|
||||
// for (var sub in _subscriptions) {
|
||||
// sub.cancel();
|
||||
// }
|
||||
// throttlerStart.dispose();
|
||||
// throttlerEnd.dispose();
|
||||
// // _playbackController.close();
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
|
||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||
import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core;
|
||||
// import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
// import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
|
||||
// import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||
// 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
|
||||
class SkipStartEnd extends _$SkipStartEnd {
|
||||
@override
|
||||
core.SkipStartEnd? build() {
|
||||
final currentBook = ref.watch(currentBookProvider);
|
||||
final bookId = currentBook?.libraryItemId;
|
||||
if (currentBook == null || bookId == null) {
|
||||
return null;
|
||||
}
|
||||
// @riverpod
|
||||
// class SkipStartEnd extends _$SkipStartEnd {
|
||||
// @override
|
||||
// core.SkipStartEnd? build() {
|
||||
// final currentBook = ref.watch(currentBookProvider);
|
||||
// final bookId = currentBook?.libraryItemId;
|
||||
// if (currentBook == null || bookId == null) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
final player = ref.read(absPlayerProvider);
|
||||
final bookSettings = ref.watch(bookSettingsProvider(bookId));
|
||||
final start = bookSettings.playerSettings.skipChapterStart;
|
||||
final end = bookSettings.playerSettings.skipChapterEnd;
|
||||
if (start < Duration.zero && end < Duration.zero) {
|
||||
return null;
|
||||
}
|
||||
// final player = ref.read(absPlayerProvider);
|
||||
// final bookSettings = ref.watch(bookSettingsProvider(bookId));
|
||||
// final start = bookSettings.playerSettings.skipChapterStart;
|
||||
// final end = bookSettings.playerSettings.skipChapterEnd;
|
||||
// if (start < Duration.zero && end < Duration.zero) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
final skipStartEnd = core.SkipStartEnd(
|
||||
start: start,
|
||||
end: end,
|
||||
player: player,
|
||||
);
|
||||
ref.onDispose(skipStartEnd.dispose);
|
||||
return skipStartEnd;
|
||||
}
|
||||
}
|
||||
// final skipStartEnd = core.SkipStartEnd(
|
||||
// start: start,
|
||||
// end: end,
|
||||
// player: player,
|
||||
// );
|
||||
// ref.onDispose(skipStartEnd.dispose);
|
||||
// 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 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:just_audio/just_audio.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
|
||||
///
|
||||
|
|
@ -32,7 +32,7 @@ class SleepTimer {
|
|||
}
|
||||
|
||||
/// The player to be paused
|
||||
final AbsAudioPlayer player;
|
||||
final AudioPlayer player;
|
||||
|
||||
/// The timer that will pause the player
|
||||
Timer? timer;
|
||||
|
|
@ -50,8 +50,8 @@ class SleepTimer {
|
|||
SleepTimer({required duration, required this.player}) : _duration = duration {
|
||||
_subscriptions.add(
|
||||
player.playerStateStream.listen((event) {
|
||||
if (event.processingState == AbsProcessingState.completed ||
|
||||
event.processingState == AbsProcessingState.idle) {
|
||||
if (event.processingState == ProcessingState.completed ||
|
||||
event.processingState == ProcessingState.idle) {
|
||||
clearCountDownTimer();
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SleepTimer extends _$SleepTimer {
|
|||
|
||||
var sleepTimer = core.SleepTimer(
|
||||
duration: sleepTimerSettings.defaultDuration,
|
||||
player: ref.watch(absPlayerProvider),
|
||||
player: ref.watch(simpleAudioPlayerProvider),
|
||||
);
|
||||
ref.onDispose(sleepTimer.dispose);
|
||||
return sleepTimer;
|
||||
|
|
@ -45,7 +45,7 @@ class SleepTimer extends _$SleepTimer {
|
|||
} else {
|
||||
final timer = core.SleepTimer(
|
||||
duration: resultingDuration,
|
||||
player: ref.watch(absPlayerProvider),
|
||||
player: ref.watch(simpleAudioPlayerProvider),
|
||||
);
|
||||
ref.onDispose(timer.dispose);
|
||||
state = timer;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$sleepTimerHash() => r'417759e07a45e69af93bd9a1c78ac859d9abcf4b';
|
||||
String _$sleepTimerHash() => r'7cac4509d8bd40c4d418c295d5b37c66492e7de9';
|
||||
|
||||
/// See also [SleepTimer].
|
||||
@ProviderFor(SleepTimer)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:vaani/api/server_provider.dart';
|
||||
import 'package:vaani/db/storage.dart';
|
||||
import 'package:vaani/features/logging/core/logger.dart';
|
||||
import 'package:vaani/features/player/core/init.dart';
|
||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||
import 'package:vaani/features/settings/api_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
|
|
@ -36,8 +37,8 @@ void main() async {
|
|||
await initStorage();
|
||||
|
||||
// initialize audio player
|
||||
// await configurePlayer();
|
||||
await container.read(configurePlayerProvider.future);
|
||||
await configurePlayer();
|
||||
// await container.read(configurePlayerProvider.future);
|
||||
// run the app
|
||||
runApp(
|
||||
UncontrolledProviderScope(
|
||||
|
|
@ -148,10 +149,12 @@ class AbsApp extends ConsumerWidget {
|
|||
final appThemeLight = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: lightColorScheme.harmonized(),
|
||||
fontFamily: fontFamilyPlatform,
|
||||
);
|
||||
final appThemeDark = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: darkColorScheme.harmonized(),
|
||||
fontFamily: fontFamilyPlatform,
|
||||
brightness: Brightness.dark,
|
||||
// TODO bottom sheet theme is not working
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ class PlayerPage extends HookConsumerWidget {
|
|||
final isVertical = size.height > size.width;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// 以下两项确保在滚动后背景色不变
|
||||
// elevation: 0 是保持 AppBar 不变的关键
|
||||
elevation: 0,
|
||||
// 设置 forceMaterialTransparency 防止滚动时的透明度变化
|
||||
forceMaterialTransparency: true,
|
||||
title: Text(currentBook.metadata.title ?? ''),
|
||||
leading: IconButton(
|
||||
iconSize: 30,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// brand color rgb(49, 27, 146) rgb(96, 76, 236)
|
||||
|
|
@ -13,3 +15,18 @@ final brandDarkColorScheme = ColorScheme.fromSeed(
|
|||
seedColor: brandColor,
|
||||
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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
13
pubspec.yaml
13
pubspec.yaml
|
|
@ -87,14 +87,15 @@ dependencies:
|
|||
|
||||
# 音频播放
|
||||
audio_service: ^0.18.15
|
||||
# audio_service_win: ^0.0.2
|
||||
audio_session: ^0.1.23
|
||||
just_audio: ^0.10.5
|
||||
# just_audio_background:
|
||||
# # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed
|
||||
# git:
|
||||
# url: https://github.com/Dr-Blank/just_audio
|
||||
# ref: media-notification-config
|
||||
# path: just_audio_background
|
||||
just_audio_background:
|
||||
# TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed
|
||||
git:
|
||||
url: https://github.com/Dr-Blank/just_audio
|
||||
ref: media-notification-config
|
||||
path: just_audio_background
|
||||
# just_audio_windows: ^0.2.2
|
||||
just_audio_media_kit: ^2.0.4
|
||||
media_kit_libs_linux: any
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue