This commit is contained in:
rang 2026-01-05 17:29:24 +08:00
parent 178f3fbdb1
commit 634ffaed8c
27 changed files with 648 additions and 1012 deletions

View file

@ -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;

View file

@ -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');

View file

@ -1,3 +0,0 @@
import 'package:just_audio/just_audio.dart';
class AudiobookPlayer extends AudioPlayer {}

View file

@ -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,
// );
// }
}

View file

@ -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,
});
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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,
// }, },
// ); );
// } },
);
}

View file

@ -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);

View file

@ -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)

View file

@ -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(),

View file

@ -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),
),
),
],
),
),
),
],
),
);
}
}

View file

@ -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:

View file

@ -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)),
), ),
); );

View file

@ -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;

View file

@ -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)

View file

@ -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();
} // }
} // }

View file

@ -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;
} // }
} // }

View file

@ -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

View file

@ -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();
} }
}), }),

View file

@ -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;

View file

@ -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)

View file

@ -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(

View file

@ -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,

View file

@ -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'; // 退
}
}

View file

@ -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:

View file

@ -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