This commit is contained in:
rang 2026-01-04 17:49:05 +08:00
parent a737365f26
commit 3c3c381f8a
18 changed files with 1266 additions and 1000 deletions

View file

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

View file

@ -1,8 +1,4 @@
import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart';
// audio_service

View file

@ -1,34 +1,62 @@
// import 'package:audio_service/audio_service.dart';
// import 'package:audio_session/audio_session.dart';
// import 'package:riverpod_annotation/riverpod_annotation.dart';
// import 'package:vaani/features/player/core/abs_audio_handler.dart' as core;
// import 'package:vaani/features/player/core/abs_audio_player.dart';
// import 'package:vaani/globals.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';
// ///
// @Riverpod(keepAlive: true)
// Future<void> configurePlayer(AbsAudioPlayer player) async {
// 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());
// await AudioService.init(
// builder: () => core.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 appSettings = loadOrCreateAppSettings();
// appLogger.finer('created simple player');
// // 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,
// },
// );
// },
// );
// }

View file

@ -2,6 +2,7 @@ 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:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as api;
@ -67,6 +68,123 @@ bool playerActive(Ref ref) {
return false;
}
@Riverpod(keepAlive: true)
AudioPlayer simpleAudioPlayer(Ref ref) {
final player = AudioPlayer();
ref.onDispose(player.dispose);
return player;
}
@Riverpod(keepAlive: true)
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)
class AbsPlayer extends _$AbsPlayer {
@ -166,7 +284,7 @@ class CurrentBook extends _$CurrentBook {
@override
api.BookExpanded? build() {
listenSelf((previous, next) {
if (next == null) {
if (previous == null && next == null) {
final activeLibraryItemId = AvailableHiveBoxes.basicBox
.getAs<String>(CacheKey.activeLibraryItemId);
if (activeLibraryItemId != null) {
@ -226,20 +344,3 @@ class CurrentChapter extends _$CurrentChapter {
Stream<Duration> positionChapter(Ref ref) {
return ref.read(absPlayerProvider).positionInChapterStream;
}
@riverpod
List<api.BookChapter> currentChapters(Ref ref) {
final book = ref.watch(currentBookProvider);
if (book == null) {
return [];
}
final currentChapter = ref.watch(currentChapterProvider);
if (currentChapter == null) {
return [];
}
final index = book.chapters.indexOf(currentChapter);
final total = book.chapters.length;
final start = index - 3 >= 0 ? index - 3 : 0;
final end = start + 20 <= total ? start + 20 : total;
return book.chapters.sublist(start, end);
}

View file

@ -57,6 +57,23 @@ 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';
/// See also [simpleAudioPlayer].
@ProviderFor(simpleAudioPlayer)
final simpleAudioPlayerProvider = Provider<AudioPlayer>.internal(
simpleAudioPlayer,
name: r'simpleAudioPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleAudioPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SimpleAudioPlayerRef = ProviderRef<AudioPlayer>;
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
/// Copied from Dart SDK
@ -225,24 +242,22 @@ final positionChapterProvider = AutoDisposeStreamProvider<Duration>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PositionChapterRef = AutoDisposeStreamProviderRef<Duration>;
String _$currentChaptersHash() => r'2d694aaa17f7eed8f97859d83e5b61f22966c35a';
String _$absAudioPlayerHash() => r'f595b5033eed9f4a4aa07c297c4a176955e6aab1';
/// See also [currentChapters].
@ProviderFor(currentChapters)
final currentChaptersProvider =
AutoDisposeProvider<List<api.BookChapter>>.internal(
currentChapters,
name: r'currentChaptersProvider',
/// See also [AbsAudioPlayer].
@ProviderFor(AbsAudioPlayer)
final absAudioPlayerProvider =
NotifierProvider<AbsAudioPlayer, AudioPlayer>.internal(
AbsAudioPlayer.new,
name: r'absAudioPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$currentChaptersHash,
: _$absAudioPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef CurrentChaptersRef = AutoDisposeProviderRef<List<api.BookChapter>>;
typedef _$AbsAudioPlayer = Notifier<AudioPlayer>;
String _$absPlayerHash() => r'e682fea03793a0370cb143602980d5c1e37396c7';
/// riverpod状态
@ -275,7 +290,7 @@ final playerStateProvider =
);
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
String _$currentBookHash() => r'790af1f9502b12879fc22c900ed5e3572381ab1e';
String _$currentBookHash() => r'714d7701508b6186598e13bc38c57c3fe644ae90';
/// See also [CurrentBook].
@ProviderFor(CurrentBook)

View file

@ -51,7 +51,7 @@ class PlayerSettings with _$PlayerSettings {
ExpandedPlayerSettings expandedPlayerSettings,
@Default(1) double preferredDefaultVolume,
@Default(1) double preferredDefaultSpeed,
@Default([1, 1.25, 1.5, 1.75, 2]) List<double> speedOptions,
@Default([0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]) List<double> speedOptions,
@Default(0.05) double speedIncrement,
@Default(0.1) double minSpeed,
@Default(4) double maxSpeed,

View file

@ -986,7 +986,15 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
this.expandedPlayerSettings = const ExpandedPlayerSettings(),
this.preferredDefaultVolume = 1,
this.preferredDefaultSpeed = 1,
final List<double> speedOptions = const [1, 1.25, 1.5, 1.75, 2],
final List<double> speedOptions = const [
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2
],
this.speedIncrement = 0.05,
this.minSpeed = 0.1,
this.maxSpeed = 4,

View file

@ -99,7 +99,7 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
speedOptions: (json['speedOptions'] as List<dynamic>?)
?.map((e) => (e as num).toDouble())
.toList() ??
const [1, 1.25, 1.5, 1.75, 2],
const [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
speedIncrement: (json['speedIncrement'] as num?)?.toDouble() ?? 0.05,
minSpeed: (json['minSpeed'] as num?)?.toDouble() ?? 0.1,
maxSpeed: (json['maxSpeed'] as num?)?.toDouble() ?? 4,

View file

@ -6,13 +6,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/models/app_settings.dart' as model;
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/features/settings/view/widgets/navigation_with_switch_tile.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/shared/widgets/custom_dropdown.dart';
class AppSettingsPage extends HookConsumerWidget {
const AppSettingsPage({
@ -40,22 +41,35 @@ class AppSettingsPage extends HookConsumerWidget {
SettingsTile(
title: Text(S.of(context).language),
leading: const Icon(Icons.language),
trailing: DropdownButton(
value: appSettings.language,
items: S.delegate.supportedLocales.map((locale) {
return DropdownMenuItem(
value: locale.languageCode,
child: Text(locales[locale.languageCode] ?? 'unknown'),
);
trailing: CustomDropdown<String>(
selected: appSettings.language,
items: (f, cs) => S.delegate.supportedLocales.map((locale) {
return locale.languageCode;
}).toList(),
onChanged: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith(
language: value!,
itemAsString: (item) => locales[item] ?? 'unknown',
onChanged: (value) async =>
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith(
language: value!,
),
),
);
},
),
// trailing: DropdownButton(
// value: appSettings.language,
// items: S.delegate.supportedLocales.map((locale) {
// return DropdownMenuItem(
// value: locale.languageCode,
// child: Text(locales[locale.languageCode] ?? 'unknown'),
// );
// }).toList(),
// onChanged: (value) {
// ref.read(appSettingsProvider.notifier).update(
// appSettings.copyWith(
// language: value!,
// ),
// );
// },
// ),
description: Text(S.of(context).languageDescription),
),
SettingsTile(
@ -67,9 +81,9 @@ class AppSettingsPage extends HookConsumerWidget {
},
),
SettingsTile(
title: Text('下载设置'),
title: Text(S.of(context).downloadSettings),
leading: const Icon(Icons.download),
description: Text('自定义下载设置'),
description: Text(S.of(context).downloadSettingsDescription),
onPressed: (context) {
context.pushNamed(Routes.downloadSettings.name);
},

View file

@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
import 'package:vaani/shared/widgets/custom_dropdown.dart';
class PlayerSettingsPage extends HookConsumerWidget {
const PlayerSettingsPage({
@ -25,8 +27,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
horizontal: AppElementSizes.paddingLarge,
vertical: AppElementSizes.paddingRegular,
),
tiles: [
// preferred settings for every book
@ -49,27 +51,26 @@ class PlayerSettingsPage extends HookConsumerWidget {
// preferred default speed
SettingsTile(
title: Text(S.of(context).playerSettingsSpeedDefault),
trailing: Text(
'${playerSettings.preferredDefaultSpeed}x',
style:
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
// trailing: Text(
// '${playerSettings.preferredDefaultSpeed}x',
// style:
// TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
// ),
trailing: CustomDropdown<double>(
selected: playerSettings.preferredDefaultSpeed,
items: (f, cs) => playerSettings.speedOptions,
itemAsString: (item) => '${item}x',
onChanged: (value) {
if (value != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
preferredDefaultSpeed: value,
),
);
}
},
),
leading: const Icon(Icons.speed),
onPressed: (context) async {
final newSpeed = await showDialog(
context: context,
builder: (context) => SpeedPicker(
initialValue: playerSettings.preferredDefaultSpeed,
),
);
if (newSpeed != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
preferredDefaultSpeed: newSpeed,
),
);
}
},
),
// preferred speed options
SettingsTile(