增加跳过片头片尾,上一章下一章移动到AudioPlayer对象中

This commit is contained in:
rang 2025-10-24 11:47:50 +08:00
parent e06c834d0e
commit 620a1eb7a2
29 changed files with 1080 additions and 179 deletions

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/generated/l10n.dart';
class DownloadsPage extends HookConsumerWidget { class DownloadsPage extends HookConsumerWidget {
const DownloadsPage({super.key}); const DownloadsPage({super.key});
@ -13,7 +14,7 @@ class DownloadsPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Downloads'), title: Text(S.of(context).bookDownloads),
), ),
body: Center( body: Center(
// history of downloads // history of downloads

View file

@ -11,6 +11,7 @@ import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/explore/providers/search_controller.dart';
import 'package:vaani/features/explore/view/search_result_page.dart'; import 'package:vaani/features/explore/view/search_result_page.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
@ -29,7 +30,7 @@ class ExplorePage extends HookConsumerWidget {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Explore'), title: Text(S.of(context).explore),
), ),
body: const MySearchBar(), body: const MySearchBar(),
); );
@ -61,8 +62,8 @@ class MySearchBar extends HookConsumerWidget {
currentQuery = query; currentQuery = query;
// In a real application, there should be some error handling here. // In a real application, there should be some error handling here.
final options = await api.libraries final options =
.search(libraryId: settings.activeLibraryId!, query: query, limit: 3); await api.libraries.search(libraryId: settings.activeLibraryId!, query: query, limit: 3);
// If another search happened after this one, throw away these options. // If another search happened after this one, throw away these options.
if (currentQuery != query) { if (currentQuery != query) {
@ -93,14 +94,11 @@ class MySearchBar extends HookConsumerWidget {
// "Seek and you shall find... your next book!" // "Seek and you shall find... your next book!"
// "Let's uncover your next favorite book..." // "Let's uncover your next favorite book..."
// "Ready to dive into a new story?" // "Ready to dive into a new story?"
hintText: 'Seek and you shall discover...', hintText: S.of(context).exploreHint,
// opacity: 0.5 for the hint text // opacity: 0.5 for the hint text
hintStyle: WidgetStatePropertyAll( hintStyle: WidgetStatePropertyAll(
Theme.of(context).textTheme.bodyMedium!.copyWith( Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context) color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
.colorScheme
.onSurface
.withValues(alpha: 0.5),
), ),
), ),
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
@ -137,7 +135,7 @@ class MySearchBar extends HookConsumerWidget {
// debugPrint('options: $options'); // debugPrint('options: $options');
if (options == null) { if (options == null) {
// TODO: show loading indicator or failure message // TODO: show loading indicator or failure message
return <Widget>[const ListTile(title: Text('Loading...'))]; return <Widget>[ListTile(title: Text(S.of(context).loading))];
} }
// see if BookLibrarySearchResponse or PodcastLibrarySearchResponse // see if BookLibrarySearchResponse or PodcastLibrarySearchResponse
if (options is BookLibrarySearchResponse) { if (options is BookLibrarySearchResponse) {
@ -233,9 +231,8 @@ class BookSearchResultMini extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull; final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull;
final image = item == null final image =
? const AsyncValue.loading() item == null ? const AsyncValue.loading() : ref.watch(coverImageProvider(item.id));
: ref.watch(coverImageProvider(item.id));
return ListTile( return ListTile(
leading: SizedBox( leading: SizedBox(
width: 50, width: 50,

View file

@ -509,6 +509,7 @@ Future<void> libraryItemPlayButtonOnPressed({
}) async { }) async {
appLogger.info('Pressed play/resume button'); appLogger.info('Pressed play/resume button');
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
// final bookSettings = ref.watch(bookSettingsProvider(book.libraryItemId));
final isCurrentBookSetInPlayer = player.book == book; final isCurrentBookSetInPlayer = player.book == book;
final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer;
@ -554,6 +555,7 @@ Future<void> libraryItemPlayButtonOnPressed({
? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed ? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed, : appPlayerSettings.preferredDefaultSpeed,
), ),
// player.setClip(start: Duration(seconds: 10)),
]); ]);
// toggle play/pause // toggle play/pause

View file

@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider;
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart' import 'package:vaani/features/you/view/widgets/library_switch_chip.dart' show showLibrarySwitcher;
show showLibrarySwitcher; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart' show Routes; import 'package:vaani/router/router.dart' show Routes;
import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons;
import 'package:vaani/shared/widgets/not_implemented.dart' import 'package:vaani/shared/widgets/not_implemented.dart' show showNotImplementedToast;
show showNotImplementedToast;
class LibraryBrowserPage extends HookConsumerWidget { class LibraryBrowserPage extends HookConsumerWidget {
const LibraryBrowserPage({super.key}); const LibraryBrowserPage({super.key});
@ -20,7 +19,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books; AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books;
// Determine the title text // Determine the title text
final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library'; final String appBarTitle = currentLibrary?.name ?? S.of(context).library;
return Scaffold( return Scaffold(
// Use CustomScrollView to enable slivers // Use CustomScrollView to enable slivers
@ -33,7 +32,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
// true, // Optional: uncomment if you want snapping behavior (usually with floating: true) // true, // Optional: uncomment if you want snapping behavior (usually with floating: true)
leading: IconButton( leading: IconButton(
icon: Icon(libraryIconData), icon: Icon(libraryIconData),
tooltip: 'Switch Library', // Helpful tooltip for users tooltip: S.of(context).librarySwitchTooltip, // Helpful tooltip for users
onPressed: () { onPressed: () {
showLibrarySwitcher(context, ref); showLibrarySwitcher(context, ref);
}, },
@ -44,7 +43,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
ListTile( ListTile(
title: const Text('Authors'), title: Text(S.of(context).bookAuthors),
leading: const Icon(Icons.person), leading: const Icon(Icons.person),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
@ -52,7 +51,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('Genres'), title: Text(S.of(context).bookGenres),
leading: const Icon(Icons.category), leading: const Icon(Icons.category),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
@ -60,7 +59,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
}, },
), ),
ListTile( ListTile(
title: const Text('Series'), title: Text(S.of(context).bookSeries),
leading: const Icon(Icons.list), leading: const Icon(Icons.list),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
@ -69,7 +68,7 @@ class LibraryBrowserPage extends HookConsumerWidget {
), ),
// Downloads // Downloads
ListTile( ListTile(
title: const Text('Downloads'), title: Text(S.of(context).bookDownloads),
leading: const Icon(Icons.download), leading: const Icon(Icons.download),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {

View file

@ -14,6 +14,8 @@ class NullablePlayerSettings with _$NullablePlayerSettings {
List<double>? speedOptions, List<double>? speedOptions,
SleepTimerSettings? sleepTimerSettings, SleepTimerSettings? sleepTimerSettings,
Duration? playbackReportInterval, Duration? playbackReportInterval,
@Default(Duration()) Duration skipChapterStart,
@Default(Duration()) Duration skipChapterEnd,
}) = _NullablePlayerSettings; }) = _NullablePlayerSettings;
factory NullablePlayerSettings.fromJson(Map<String, dynamic> json) => factory NullablePlayerSettings.fromJson(Map<String, dynamic> json) =>

View file

@ -31,6 +31,8 @@ mixin _$NullablePlayerSettings {
SleepTimerSettings? get sleepTimerSettings => SleepTimerSettings? get sleepTimerSettings =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
Duration? get playbackReportInterval => throw _privateConstructorUsedError; Duration? get playbackReportInterval => throw _privateConstructorUsedError;
Duration get skipChapterStart => throw _privateConstructorUsedError;
Duration get skipChapterEnd => throw _privateConstructorUsedError;
/// Serializes this NullablePlayerSettings to a JSON map. /// Serializes this NullablePlayerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -55,7 +57,9 @@ abstract class $NullablePlayerSettingsCopyWith<$Res> {
double? preferredDefaultSpeed, double? preferredDefaultSpeed,
List<double>? speedOptions, List<double>? speedOptions,
SleepTimerSettings? sleepTimerSettings, SleepTimerSettings? sleepTimerSettings,
Duration? playbackReportInterval}); Duration? playbackReportInterval,
Duration skipChapterStart,
Duration skipChapterEnd});
$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;
$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings; $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;
@ -85,6 +89,8 @@ class _$NullablePlayerSettingsCopyWithImpl<$Res,
Object? speedOptions = freezed, Object? speedOptions = freezed,
Object? sleepTimerSettings = freezed, Object? sleepTimerSettings = freezed,
Object? playbackReportInterval = freezed, Object? playbackReportInterval = freezed,
Object? skipChapterStart = null,
Object? skipChapterEnd = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
miniPlayerSettings: freezed == miniPlayerSettings miniPlayerSettings: freezed == miniPlayerSettings
@ -115,6 +121,14 @@ class _$NullablePlayerSettingsCopyWithImpl<$Res,
? _value.playbackReportInterval ? _value.playbackReportInterval
: playbackReportInterval // ignore: cast_nullable_to_non_nullable : playbackReportInterval // ignore: cast_nullable_to_non_nullable
as Duration?, as Duration?,
skipChapterStart: null == skipChapterStart
? _value.skipChapterStart
: skipChapterStart // ignore: cast_nullable_to_non_nullable
as Duration,
skipChapterEnd: null == skipChapterEnd
? _value.skipChapterEnd
: skipChapterEnd // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val); ) as $Val);
} }
@ -180,7 +194,9 @@ abstract class _$$NullablePlayerSettingsImplCopyWith<$Res>
double? preferredDefaultSpeed, double? preferredDefaultSpeed,
List<double>? speedOptions, List<double>? speedOptions,
SleepTimerSettings? sleepTimerSettings, SleepTimerSettings? sleepTimerSettings,
Duration? playbackReportInterval}); Duration? playbackReportInterval,
Duration skipChapterStart,
Duration skipChapterEnd});
@override @override
$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;
@ -212,6 +228,8 @@ class __$$NullablePlayerSettingsImplCopyWithImpl<$Res>
Object? speedOptions = freezed, Object? speedOptions = freezed,
Object? sleepTimerSettings = freezed, Object? sleepTimerSettings = freezed,
Object? playbackReportInterval = freezed, Object? playbackReportInterval = freezed,
Object? skipChapterStart = null,
Object? skipChapterEnd = null,
}) { }) {
return _then(_$NullablePlayerSettingsImpl( return _then(_$NullablePlayerSettingsImpl(
miniPlayerSettings: freezed == miniPlayerSettings miniPlayerSettings: freezed == miniPlayerSettings
@ -242,6 +260,14 @@ class __$$NullablePlayerSettingsImplCopyWithImpl<$Res>
? _value.playbackReportInterval ? _value.playbackReportInterval
: playbackReportInterval // ignore: cast_nullable_to_non_nullable : playbackReportInterval // ignore: cast_nullable_to_non_nullable
as Duration?, as Duration?,
skipChapterStart: null == skipChapterStart
? _value.skipChapterStart
: skipChapterStart // ignore: cast_nullable_to_non_nullable
as Duration,
skipChapterEnd: null == skipChapterEnd
? _value.skipChapterEnd
: skipChapterEnd // ignore: cast_nullable_to_non_nullable
as Duration,
)); ));
} }
} }
@ -256,7 +282,9 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
this.preferredDefaultSpeed, this.preferredDefaultSpeed,
final List<double>? speedOptions, final List<double>? speedOptions,
this.sleepTimerSettings, this.sleepTimerSettings,
this.playbackReportInterval}) this.playbackReportInterval,
this.skipChapterStart = const Duration(),
this.skipChapterEnd = const Duration()})
: _speedOptions = speedOptions; : _speedOptions = speedOptions;
factory _$NullablePlayerSettingsImpl.fromJson(Map<String, dynamic> json) => factory _$NullablePlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
@ -284,10 +312,16 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
final SleepTimerSettings? sleepTimerSettings; final SleepTimerSettings? sleepTimerSettings;
@override @override
final Duration? playbackReportInterval; final Duration? playbackReportInterval;
@override
@JsonKey()
final Duration skipChapterStart;
@override
@JsonKey()
final Duration skipChapterEnd;
@override @override
String toString() { String toString() {
return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval, skipChapterStart: $skipChapterStart, skipChapterEnd: $skipChapterEnd)';
} }
@override @override
@ -308,7 +342,11 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
(identical(other.sleepTimerSettings, sleepTimerSettings) || (identical(other.sleepTimerSettings, sleepTimerSettings) ||
other.sleepTimerSettings == sleepTimerSettings) && other.sleepTimerSettings == sleepTimerSettings) &&
(identical(other.playbackReportInterval, playbackReportInterval) || (identical(other.playbackReportInterval, playbackReportInterval) ||
other.playbackReportInterval == playbackReportInterval)); other.playbackReportInterval == playbackReportInterval) &&
(identical(other.skipChapterStart, skipChapterStart) ||
other.skipChapterStart == skipChapterStart) &&
(identical(other.skipChapterEnd, skipChapterEnd) ||
other.skipChapterEnd == skipChapterEnd));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -321,7 +359,9 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
preferredDefaultSpeed, preferredDefaultSpeed,
const DeepCollectionEquality().hash(_speedOptions), const DeepCollectionEquality().hash(_speedOptions),
sleepTimerSettings, sleepTimerSettings,
playbackReportInterval); playbackReportInterval,
skipChapterStart,
skipChapterEnd);
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -348,7 +388,9 @@ abstract class _NullablePlayerSettings implements NullablePlayerSettings {
final double? preferredDefaultSpeed, final double? preferredDefaultSpeed,
final List<double>? speedOptions, final List<double>? speedOptions,
final SleepTimerSettings? sleepTimerSettings, final SleepTimerSettings? sleepTimerSettings,
final Duration? playbackReportInterval}) = _$NullablePlayerSettingsImpl; final Duration? playbackReportInterval,
final Duration skipChapterStart,
final Duration skipChapterEnd}) = _$NullablePlayerSettingsImpl;
factory _NullablePlayerSettings.fromJson(Map<String, dynamic> json) = factory _NullablePlayerSettings.fromJson(Map<String, dynamic> json) =
_$NullablePlayerSettingsImpl.fromJson; _$NullablePlayerSettingsImpl.fromJson;
@ -367,6 +409,10 @@ abstract class _NullablePlayerSettings implements NullablePlayerSettings {
SleepTimerSettings? get sleepTimerSettings; SleepTimerSettings? get sleepTimerSettings;
@override @override
Duration? get playbackReportInterval; Duration? get playbackReportInterval;
@override
Duration get skipChapterStart;
@override
Duration get skipChapterEnd;
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -32,6 +32,12 @@ _$NullablePlayerSettingsImpl _$$NullablePlayerSettingsImplFromJson(
? null ? null
: Duration( : Duration(
microseconds: (json['playbackReportInterval'] as num).toInt()), microseconds: (json['playbackReportInterval'] as num).toInt()),
skipChapterStart: json['skipChapterStart'] == null
? const Duration()
: Duration(microseconds: (json['skipChapterStart'] as num).toInt()),
skipChapterEnd: json['skipChapterEnd'] == null
? const Duration()
: Duration(microseconds: (json['skipChapterEnd'] as num).toInt()),
); );
Map<String, dynamic> _$$NullablePlayerSettingsImplToJson( Map<String, dynamic> _$$NullablePlayerSettingsImplToJson(
@ -44,4 +50,6 @@ Map<String, dynamic> _$$NullablePlayerSettingsImplToJson(
'speedOptions': instance.speedOptions, 'speedOptions': instance.speedOptions,
'sleepTimerSettings': instance.sleepTimerSettings, 'sleepTimerSettings': instance.sleepTimerSettings,
'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds, 'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds,
'skipChapterStart': instance.skipChapterStart.inMicroseconds,
'skipChapterEnd': instance.skipChapterEnd.inMicroseconds,
}; };

View file

@ -1,8 +1,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart' import 'package:vaani/features/per_book_settings/models/book_settings.dart' as model;
as model;
import 'package:vaani/features/per_book_settings/models/nullable_player_settings.dart'; import 'package:vaani/features/per_book_settings/models/nullable_player_settings.dart';
part 'book_settings_provider.g.dart'; part 'book_settings_provider.g.dart';
@ -52,6 +51,7 @@ class BookSettings extends _$BookSettings {
} }
void update(model.BookSettings newSettings, {bool force = false}) { void update(model.BookSettings newSettings, {bool force = false}) {
state = newSettings;
updateState(newSettings, force: force); updateState(newSettings, force: force);
} }
} }

View file

@ -6,7 +6,7 @@ part of 'book_settings_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$bookSettingsHash() => r'b976df954edf98ec6ccb3eb41e9d07dd4a9193eb'; String _$bookSettingsHash() => r'ef4316367513b1b2b3971e53609e8f0f29de8667';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View file

@ -51,7 +51,18 @@ class AudiobookPlayer extends AudioPlayer {
AudiobookPlayer(this.token, this.baseUrl) : super() { AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book // set the source of the player to the first track in the book
_logger.config('Setting up audiobook player'); _logger.config('Setting up audiobook player');
// currentIndexStream.listen((index) {
// print('播放器已切换到第 $index 首曲目');
// skip = true;
// });
// positionStream.listen((position) {
// if (skip != null && skip! && position.inSeconds < 20) {
// super.seek(Duration(seconds: 20));
// print('播放 $position');
// }
// });
} }
bool? skip;
/// the [BookExpanded] being played /// the [BookExpanded] being played
BookExpanded? _book; BookExpanded? _book;
@ -114,9 +125,8 @@ class AudiobookPlayer extends AudioPlayer {
// initialPosition ; // initialPosition ;
final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero); final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay); final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null final initialPositionInTrack =
? initialPosition - trackToPlay.startOffset initialPosition != null ? initialPosition - trackToPlay.startOffset : null;
: null;
_logger.finer('Setting audioSource'); _logger.finer('Setting audioSource');
await setAudioSource( await setAudioSource(
@ -126,8 +136,7 @@ class AudiobookPlayer extends AudioPlayer {
ConcatenatingAudioSource( ConcatenatingAudioSource(
useLazyPreparation: true, useLazyPreparation: true,
children: book.tracks.map((track) { children: book.tracks.map((track) {
final retrievedUri = final retrievedUri = _getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
_logger.fine( _logger.fine(
'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}', 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
); );
@ -137,10 +146,8 @@ class AudiobookPlayer extends AudioPlayer {
// Specify a unique ID for each media item: // Specify a unique ID for each media item:
id: book.libraryItemId + track.index.toString(), id: book.libraryItemId + track.index.toString(),
// Metadata to display in the notification: // Metadata to display in the notification:
title: appSettings.notificationSettings.primaryTitle title: appSettings.notificationSettings.primaryTitle.formatNotificationTitle(book),
.formatNotificationTitle(book), album: appSettings.notificationSettings.secondaryTitle.formatNotificationTitle(book),
album: appSettings.notificationSettings.secondaryTitle
.formatNotificationTitle(book),
artUri: artworkUri ?? artUri: artworkUri ??
Uri.parse( Uri.parse(
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
@ -172,7 +179,10 @@ class AudiobookPlayer extends AudioPlayer {
/// so we need to calculate the duration and current position based on the book /// so we need to calculate the duration and current position based on the book
@override @override
Future<void> seek(Duration? positionInBook, {int? index}) async { Future<void> seek(Duration? positionInBook, {int? index, bool b = true}) async {
if (!b) {
return super.seek(positionInBook, index: index);
}
if (_book == null) { if (_book == null) {
_logger.warning('No book is set, not seeking'); _logger.warning('No book is set, not seeking');
return; return;
@ -183,9 +193,43 @@ class AudiobookPlayer extends AudioPlayer {
} }
final tracks = _book!.tracks; final tracks = _book!.tracks;
final trackToPlay = getTrackToPlay(_book!, positionInBook); final trackToPlay = getTrackToPlay(_book!, positionInBook);
final index = tracks.indexOf(trackToPlay); final i = tracks.indexOf(trackToPlay);
final positionInTrack = positionInBook - trackToPlay.startOffset; final positionInTrack = positionInBook - trackToPlay.startOffset;
return super.seek(positionInTrack, index: index); return super.seek(positionInTrack, index: i);
}
// add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
final doNotSeekBackIfLessThan = Duration(seconds: 5);
/// seek forward to the next chapter
void seekForward() {
final index = _book!.chapters.indexOf(currentChapter!);
if (index < _book!.chapters.length - 1) {
seek(
_book!.chapters[index + 1].start + offset,
);
} else {
seek(currentChapter!.end);
}
}
/// seek backward to the previous chapter or the start of the current chapter
void seekBackward() {
final currentPlayingChapterIndex = _book!.chapters.indexOf(currentChapter!);
final chapterPosition = positionInBook - currentChapter!.start;
BookChapter chapterToSeekTo;
// if player position is less than 5 seconds into the chapter, go to the previous chapter
if (chapterPosition < doNotSeekBackIfLessThan && currentPlayingChapterIndex > 0) {
chapterToSeekTo = _book!.chapters[currentPlayingChapterIndex - 1];
} else {
chapterToSeekTo = currentChapter!;
}
seek(
chapterToSeekTo.start + offset,
);
} }
/// a convenience method to get position in the book instead of the current track position /// a convenience method to get position in the book instead of the current track position
@ -201,8 +245,7 @@ class AudiobookPlayer extends AudioPlayer {
if (_book == null) { if (_book == null) {
return Duration.zero; return Duration.zero;
} }
return bufferedPosition + return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset;
_book!.tracks[sequenceState!.currentIndex].startOffset;
} }
/// streams to override to suit the book instead of the current track /// streams to override to suit the book instead of the current track
@ -277,8 +320,7 @@ Uri _getUri(
}, },
); );
return uri ?? return uri ?? Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
} }
extension FormatNotificationTitle on String { extension FormatNotificationTitle on String {

View file

@ -5,6 +5,7 @@ import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/providers/currently_playing_provider.dart';
import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/features/player/view/audiobook_player.dart'; import 'package:vaani/features/player/view/audiobook_player.dart';
import 'package:vaani/features/skip_start_end/player_skip_chapter_start_end.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/not_implemented.dart'; import 'package:vaani/shared/widgets/not_implemented.dart';
@ -245,6 +246,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
const Spacer(), const Spacer(),
// chapter list // chapter list
const ChapterSelectionButton(), const ChapterSelectionButton(),
const Spacer(),
//
SkipChapterStartEndButton(),
// settings // settings
// IconButton( // IconButton(
// icon: const Icon(Icons.more_horiz), // icon: const Icon(Icons.more_horiz),

View file

@ -17,42 +17,42 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
// add a small offset so the display does not show the previous chapter for a split second // // add a small offset so the display does not show the previous chapter for a split second
const offset = Duration(milliseconds: 10); // const offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter // /// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
const doNotSeekBackIfLessThan = Duration(seconds: 5); // const doNotSeekBackIfLessThan = Duration(seconds: 5);
/// seek forward to the next chapter // /// seek forward to the next chapter
void seekForward() { // void seekForward() {
final index = player.book!.chapters.indexOf(player.currentChapter!); // final index = player.book!.chapters.indexOf(player.currentChapter!);
if (index < player.book!.chapters.length - 1) { // if (index < player.book!.chapters.length - 1) {
player.seek( // player.seek(
player.book!.chapters[index + 1].start + offset, // player.book!.chapters[index + 1].start + offset,
); // );
} else { // } else {
player.seek(player.currentChapter!.end); // player.seek(player.currentChapter!.end);
} // }
} // }
/// seek backward to the previous chapter or the start of the current chapter // /// seek backward to the previous chapter or the start of the current chapter
void seekBackward() { // void seekBackward() {
final currentPlayingChapterIndex = // final currentPlayingChapterIndex =
player.book!.chapters.indexOf(player.currentChapter!); // player.book!.chapters.indexOf(player.currentChapter!);
final chapterPosition = // final chapterPosition =
player.positionInBook - player.currentChapter!.start; // player.positionInBook - player.currentChapter!.start;
BookChapter chapterToSeekTo; // BookChapter chapterToSeekTo;
// if player position is less than 5 seconds into the chapter, go to the previous chapter // // if player position is less than 5 seconds into the chapter, go to the previous chapter
if (chapterPosition < doNotSeekBackIfLessThan && // if (chapterPosition < doNotSeekBackIfLessThan &&
currentPlayingChapterIndex > 0) { // currentPlayingChapterIndex > 0) {
chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1]; // chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1];
} else { // } else {
chapterToSeekTo = player.currentChapter!; // chapterToSeekTo = player.currentChapter!;
} // }
player.seek( // player.seek(
chapterToSeekTo.start + offset, // chapterToSeekTo.start + offset,
); // );
} // }
return IconButton( return IconButton(
icon: Icon( icon: Icon(
@ -69,9 +69,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
return; return;
} }
if (isForward) { if (isForward) {
seekForward(); player.seekForward();
} else { } else {
seekBackward(); player.seekBackward();
} }
}, },
); );

View file

@ -1,17 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart' import 'package:vaani/features/player/providers/audiobook_player.dart' show audiobookPlayerProvider;
show audiobookPlayerProvider;
import 'package:vaani/features/player/providers/currently_playing_provider.dart' import 'package:vaani/features/player/providers/currently_playing_provider.dart'
show currentPlayingChapterProvider, currentlyPlayingBookProvider; show currentPlayingChapterProvider, currentlyPlayingBookProvider;
import 'package:vaani/features/player/view/player_when_expanded.dart' import 'package:vaani/features/player/view/player_when_expanded.dart' show pendingPlayerModals;
show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
import 'package:vaani/main.dart' show appLogger; import 'package:vaani/main.dart' show appLogger;
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
import 'package:vaani/shared/extensions/duration_format.dart' import 'package:vaani/shared/extensions/duration_format.dart' show DurationFormat;
show DurationFormat;
import 'package:vaani/shared/hooks.dart' show useTimer; import 'package:vaani/shared/hooks.dart' show useTimer;
class ChapterSelectionButton extends HookConsumerWidget { class ChapterSelectionButton extends HookConsumerWidget {
@ -70,7 +67,7 @@ class ChapterSelectionModal extends HookConsumerWidget {
); );
} }
useTimer(scrollToCurrentChapter, 500.ms); useTimer(scrollToCurrentChapter, 100.ms);
// useInterval(scrollToCurrentChapter, 500.ms); // useInterval(scrollToCurrentChapter, 500.ms);
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
@ -84,19 +81,18 @@ class ChapterSelectionModal extends HookConsumerWidget {
Expanded( Expanded(
child: Scrollbar( child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
primary: true,
child: currentBook?.chapters == null child: currentBook?.chapters == null
? const Text('No chapters found') ? const Text('No chapters found')
: Column( : Column(
children: currentBook!.chapters.map( children: currentBook!.chapters.map(
(chapter) { (chapter) {
final isCurrent = currentChapterIndex == chapter.id; final isCurrent = currentChapterIndex == chapter.id;
final isPlayed = currentChapterIndex != null && final isPlayed =
chapter.id < currentChapterIndex; currentChapterIndex != null && chapter.id < currentChapterIndex;
return ListTile( return ListTile(
autofocus: isCurrent, autofocus: isCurrent,
iconColor: isPlayed && !isCurrent iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
? theme.disabledColor
: null,
title: Text( title: Text(
chapter.title, chapter.title,
style: isPlayed && !isCurrent style: isPlayed && !isCurrent

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/view/player_when_expanded.dart';
import 'package:vaani/settings/view/notification_settings_page.dart';
class SkipChapterStartEndButton extends HookConsumerWidget {
const SkipChapterStartEndButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Tooltip(
message: "跳过片头片尾",
child: IconButton(
icon: const Icon(Icons.fast_forward_rounded),
onPressed: () async {
// show toast
pendingPlayerModals++;
await showModalBottomSheet<bool>(
context: context,
barrierLabel: '跳过片头片尾',
constraints: BoxConstraints(
// 40% of the screen height
maxHeight: MediaQuery.of(context).size.height * 0.4,
),
builder: (context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: PlayerSkipChapterStartEnd(),
);
},
);
pendingPlayerModals--;
},
),
);
}
}
class PlayerSkipChapterStartEnd extends HookConsumerWidget {
const PlayerSkipChapterStartEnd({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_';
final bookSettings = ref.watch(bookSettingsProvider(bookId));
return Scaffold(
body: Column(
children: [
ListTile(
title: Text('跳过片头 ${bookSettings.playerSettings.skipChapterStart.inSeconds}s'),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: bookSettings.playerSettings.skipChapterStart,
// defaultValue: const Duration(seconds: 0),
min: const Duration(seconds: 0),
max: const Duration(seconds: 60),
step: const Duration(seconds: 1),
onChangedEnd: (interval) {
ref
.read(
bookSettingsProvider(bookId).notifier,
)
.update(
bookSettings.copyWith.playerSettings(skipChapterStart: interval),
);
ref.read(audiobookPlayerProvider).setClip(start: interval);
},
),
),
ListTile(
title: Text('跳过片尾 ${bookSettings.playerSettings.skipChapterEnd.inSeconds}s'),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: bookSettings.playerSettings.skipChapterEnd,
// defaultValue: const Duration(seconds: 0),
min: const Duration(seconds: 0),
max: const Duration(seconds: 60),
step: const Duration(seconds: 1),
onChangedEnd: (interval) {
ref
.read(
bookSettingsProvider(bookId).notifier,
)
.update(
bookSettings.copyWith.playerSettings(skipChapterEnd: interval),
);
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,86 @@
import 'dart:async';
import 'package:vaani/features/player/core/audiobook_player.dart';
class SkipStartEnd {
final Duration start;
final Duration end;
final AudiobookPlayer player;
int _index;
final List<StreamSubscription> _subscriptions = [];
final throttler = Throttler(delay: Duration(seconds: 3));
// final StreamController<PlaybackEvent> _playbackController =
// StreamController<PlaybackEvent>.broadcast();
SkipStartEnd({required this.start, required this.end, required this.player}) : _index = 0 {
if (start > Duration()) {
_subscriptions.add(
player.currentIndexStream.listen((index) {
if (_index != index && player.position.inMilliseconds < 500) {
_index = index!;
Future.microtask(() {
player.seek(start, b: false);
});
}
}),
);
}
if (end > Duration()) {
_subscriptions.add(
player.positionStream.distinct().listen((position) {
if (player.duration != null &&
player.duration!.inMilliseconds - player.position.inMilliseconds <
end.inMilliseconds) {
Future.microtask(() {
throttler.call(player.seekForward);
});
}
}),
);
}
}
/// dispose the timer
void dispose() {
for (var sub in _subscriptions) {
sub.cancel();
}
throttler.dispose();
// _playbackController.close();
}
}
class Throttler {
final Duration delay;
Timer? _timer;
DateTime? _lastRun;
Throttler({required this.delay});
void call(void Function() callback) {
//
if (_lastRun == null) {
callback();
_lastRun = DateTime.now();
return;
}
//
if (DateTime.now().difference(_lastRun!) > delay) {
callback();
_lastRun = DateTime.now();
}
//
else {
_timer?.cancel();
_timer = Timer(delay, () {
callback();
_lastRun = DateTime.now();
});
}
}
void dispose() {
_timer?.cancel();
}
}

View file

@ -0,0 +1,23 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core;
part 'skip_start_end_provider.g.dart';
@Riverpod(keepAlive: true)
class SkipStartEnd extends _$SkipStartEnd {
@override
core.SkipStartEnd? build() {
final player = ref.watch(audiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_';
if (bookId == '_') {
return null;
}
final bookSettings = ref.watch(bookSettingsProvider(bookId));
final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd;
return core.SkipStartEnd(start: start, end: end, player: player);
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'skip_start_end_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$skipStartEndHash() => r'202cfb36fdb3d3fa12debfb188f87650473a88a9';
/// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd)
final skipStartEndProvider =
NotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
SkipStartEnd.new,
name: r'skipStartEndProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SkipStartEnd = Notifier<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

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' show Library; import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
import 'package:vaani/api/library_provider.dart'; import 'package:vaani/api/library_provider.dart';
import 'package:vaani/settings/api_settings_provider.dart' import 'package:vaani/generated/l10n.dart';
show apiSettingsProvider; import 'package:vaani/settings/api_settings_provider.dart' show apiSettingsProvider;
import 'package:vaani/shared/icons/abs_icons.dart'; import 'package:vaani/shared/icons/abs_icons.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
@ -33,7 +33,7 @@ class LibrarySwitchChip extends HookConsumerWidget {
: libraries.first.icon, : libraries.first.icon,
), ),
), // Replace with your icon ), // Replace with your icon
label: const Text('Change Library'), label: Text(S.of(context).libraryChange),
// Enable only if libraries are loaded and not empty // Enable only if libraries are loaded and not empty
onPressed: libraries.isNotEmpty onPressed: libraries.isNotEmpty
? () => showLibrarySwitcher( ? () => showLibrarySwitcher(
@ -69,7 +69,7 @@ void showLibrarySwitcher(
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: const Text('Select Library'), title: Text(S.of(context).librarySelect),
content: SizedBox( content: SizedBox(
// Constrain size for dialogs // Constrain size for dialogs
width: 300, // Adjust as needed width: 300, // Adjust as needed
@ -83,11 +83,11 @@ void showLibrarySwitcher(
ref.invalidate(librariesProvider); ref.invalidate(librariesProvider);
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
}, },
child: const Text('Refresh'), child: Text(S.of(context).refresh),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(dialogContext), onPressed: () => Navigator.pop(dialogContext),
child: const Text('Cancel'), child: Text(S.of(context).cancel),
), ),
], ],
), ),
@ -99,8 +99,7 @@ void showLibrarySwitcher(
// Make it scrollable and control height // Make it scrollable and control height
isScrollControlled: true, isScrollControlled: true,
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: maxHeight: MediaQuery.of(context).size.height * 0.6, // Max 60% of screen
MediaQuery.of(context).size.height * 0.6, // Max 60% of screen
), ),
builder: (sheetContext) => Padding( builder: (sheetContext) => Padding(
// Add padding within the bottom sheet // Add padding within the bottom sheet
@ -108,8 +107,8 @@ void showLibrarySwitcher(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, // Take minimum necessary height mainAxisSize: MainAxisSize.min, // Take minimum necessary height
children: [ children: [
const Text( Text(
'Select Library', S.of(context).librarySelect,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -121,7 +120,7 @@ void showLibrarySwitcher(
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text('Refresh'), label: Text(S.of(context).refresh),
onPressed: () { onPressed: () {
// Invalidate the provider to trigger a refetch // Invalidate the provider to trigger a refetch
ref.invalidate(librariesProvider); ref.invalidate(librariesProvider);
@ -157,14 +156,14 @@ class _LibrarySelectionContent extends ConsumerWidget {
Icon(Icons.error_outline, color: errorColor), Icon(Icons.error_outline, color: errorColor),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'Error loading libraries: $error', S.of(context).libraryLoadError('$error'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: errorColor), style: TextStyle(color: errorColor),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text('Retry'), label: Text(S.of(context).retry),
onPressed: () { onPressed: () {
// Invalidate the provider to trigger a refetch // Invalidate the provider to trigger a refetch
ref.invalidate(librariesProvider); ref.invalidate(librariesProvider);
@ -179,10 +178,10 @@ class _LibrarySelectionContent extends ConsumerWidget {
data: (libraries) { data: (libraries) {
// Handle case where data loaded successfully but is empty // Handle case where data loaded successfully but is empty
if (libraries.isEmpty) { if (libraries.isEmpty) {
return const Center( return Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Text('No libraries available.'), child: Text(S.of(context).libraryEmpty),
), ),
); );
} }

View file

@ -5,6 +5,7 @@ import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_provider.dart' show librariesProvider; import 'package:vaani/api/library_provider.dart' show librariesProvider;
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/constants.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
@ -25,7 +26,7 @@ class YouPage extends HookConsumerWidget {
// title: const Text('You'), // title: const Text('You'),
actions: [ actions: [
IconButton( IconButton(
tooltip: 'Logs', tooltip: S.of(context).logs,
icon: const Icon(Icons.bug_report), icon: const Icon(Icons.bug_report),
onPressed: () { onPressed: () {
context.pushNamed(Routes.logs.name); context.pushNamed(Routes.logs.name);
@ -38,7 +39,7 @@ class YouPage extends HookConsumerWidget {
// }, // },
// ), // ),
IconButton( IconButton(
tooltip: 'Settings', tooltip: S.of(context).settings,
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
onPressed: () { onPressed: () {
context.pushNamed(Routes.settings.name); context.pushNamed(Routes.settings.name);
@ -61,14 +62,13 @@ class YouPage extends HookConsumerWidget {
children: [ children: [
ActionChip( ActionChip(
avatar: const Icon(Icons.switch_account_outlined), avatar: const Icon(Icons.switch_account_outlined),
label: const Text('Switch Account'), label: Text(S.of(context).accountSwitch),
onPressed: () { onPressed: () {
context.pushNamed(Routes.userManagement.name); context.pushNamed(Routes.userManagement.name);
}, },
), ),
librariesAsyncValue.when( librariesAsyncValue.when(
data: (libraries) => data: (libraries) => LibrarySwitchChip(libraries: libraries),
LibrarySwitchChip(libraries: libraries),
loading: () => const ActionChip( loading: () => const ActionChip(
avatar: SizedBox( avatar: SizedBox(
width: 18, width: 18,
@ -88,13 +88,13 @@ class YouPage extends HookConsumerWidget {
// Maybe show error details or allow retry // Maybe show error details or allow retry
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content: Text('Failed to load libraries: $error'),
Text('Failed to load libraries: $error'),
), ),
); );
}, },
), ),
), // ActionChip( ),
// ActionChip(
// avatar: const Icon(Icons.logout), // avatar: const Icon(Icons.logout),
// label: const Text('Logout'), // label: const Text('Logout'),
// onPressed: () { // onPressed: () {
@ -113,7 +113,7 @@ class YouPage extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( ListTile(
leading: const Icon(Icons.playlist_play), leading: const Icon(Icons.playlist_play),
title: const Text('My Playlists'), title: Text(S.of(context).playlistsMine),
onTap: () { onTap: () {
// Handle navigation to playlists // Handle navigation to playlists
showNotImplementedToast(context); showNotImplementedToast(context);
@ -121,7 +121,7 @@ class YouPage extends HookConsumerWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.web), leading: const Icon(Icons.web),
title: const Text('Web Version'), title: Text(S.of(context).webVersion),
onTap: () { onTap: () {
handleLaunchUrl( handleLaunchUrl(
// get url from api and launch it // get url from api and launch it
@ -131,7 +131,7 @@ class YouPage extends HookConsumerWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.help), leading: const Icon(Icons.help),
title: const Text('Help'), title: Text(S.of(context).help),
onTap: () { onTap: () {
// Handle navigation to help website // Handle navigation to help website
showNotImplementedToast(context); showNotImplementedToast(context);
@ -141,8 +141,7 @@ class YouPage extends HookConsumerWidget {
icon: const Icon(Icons.info), icon: const Icon(Icons.info),
applicationName: AppMetadata.appName, applicationName: AppMetadata.appName,
applicationVersion: AppMetadata.version, applicationVersion: AppMetadata.version,
applicationLegalese: applicationLegalese: 'Made with ❤️ by ${AppMetadata.author}',
'Made with ❤️ by ${AppMetadata.author}',
aboutBoxChildren: [ aboutBoxChildren: [
// link to github repo // link to github repo
ListTile( ListTile(
@ -217,8 +216,7 @@ class UserBar extends HookConsumerWidget {
Text( Text(
api.baseUrl.toString(), api.baseUrl.toString(),
style: textTheme.bodyMedium?.copyWith( style: textTheme.bodyMedium?.copyWith(
color: color: themeData.colorScheme.onSurface.withValues(alpha: 0.6),
themeData.colorScheme.onSurface.withValues(alpha: 0.6),
), ),
), ),
], ],

View file

@ -24,8 +24,12 @@ class MessageLookup extends MessageLookupByLibrary {
static String m1(item) => "Deleted ${item}"; static String m1(item) => "Deleted ${item}";
static String m2(error) => "Error loading libraries: ${error}";
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"account": MessageLookupByLibrary.simpleMessage("Account"),
"accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"),
"appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"),
"appearance": MessageLookupByLibrary.simpleMessage("Appearance"), "appearance": MessageLookupByLibrary.simpleMessage("Appearance"),
"autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage(
@ -42,12 +46,20 @@ class MessageLookup extends MessageLookupByLibrary {
"bookAboutDefault": MessageLookupByLibrary.simpleMessage( "bookAboutDefault": MessageLookupByLibrary.simpleMessage(
"Sorry, no description found", "Sorry, no description found",
), ),
"bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"),
"bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"),
"bookGenres": MessageLookupByLibrary.simpleMessage("Genres"),
"bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"),
"bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"),
"bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"),
"bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage(
"Unabridged", "Unabridged",
), ),
"bookSeries": MessageLookupByLibrary.simpleMessage("Series"),
"bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"),
"bookShelveEmptyText": MessageLookupByLibrary.simpleMessage(
"No shelves to display",
),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage( "copyToClipboard": MessageLookupByLibrary.simpleMessage(
"Copy to Clipboard", "Copy to Clipboard",
@ -62,11 +74,30 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteDialog": m0, "deleteDialog": m0,
"deleted": m1, "deleted": m1,
"explore": MessageLookupByLibrary.simpleMessage("explore"), "explore": MessageLookupByLibrary.simpleMessage("explore"),
"exploreHint": MessageLookupByLibrary.simpleMessage(
"Seek and you shall discover...",
),
"exploreTooltip": MessageLookupByLibrary.simpleMessage( "exploreTooltip": MessageLookupByLibrary.simpleMessage(
"Search and Explore", "Search and Explore",
), ),
"general": MessageLookupByLibrary.simpleMessage("General"), "general": MessageLookupByLibrary.simpleMessage("General"),
"help": MessageLookupByLibrary.simpleMessage("Help"),
"home": MessageLookupByLibrary.simpleMessage("Home"), "home": MessageLookupByLibrary.simpleMessage("Home"),
"homeBookContinueListening": MessageLookupByLibrary.simpleMessage(
"Continue Listening",
),
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage(
"Continue Series",
),
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("Discover"),
"homeBookListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"),
"homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage(
"Newest Authors",
),
"homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage(
"Recently Added",
),
"homeBookRecommended": MessageLookupByLibrary.simpleMessage("Recommended"),
"homeContinueListening": MessageLookupByLibrary.simpleMessage( "homeContinueListening": MessageLookupByLibrary.simpleMessage(
"Continue Listening", "Continue Listening",
), ),
@ -85,10 +116,22 @@ class MessageLookup extends MessageLookupByLibrary {
"Language switch", "Language switch",
), ),
"library": MessageLookupByLibrary.simpleMessage("Library"), "library": MessageLookupByLibrary.simpleMessage("Library"),
"libraryChange": MessageLookupByLibrary.simpleMessage("Change Library"),
"libraryEmpty": MessageLookupByLibrary.simpleMessage(
"No libraries available.",
),
"libraryLoadError": m2,
"librarySelect": MessageLookupByLibrary.simpleMessage("Select Library"),
"librarySwitchTooltip": MessageLookupByLibrary.simpleMessage(
"Switch Library",
),
"libraryTooltip": MessageLookupByLibrary.simpleMessage( "libraryTooltip": MessageLookupByLibrary.simpleMessage(
"Browse your library", "Browse your library",
), ),
"loading": MessageLookupByLibrary.simpleMessage("Loading..."),
"logs": MessageLookupByLibrary.simpleMessage("Logs"),
"no": MessageLookupByLibrary.simpleMessage("No"), "no": MessageLookupByLibrary.simpleMessage("No"),
"notImplemented": MessageLookupByLibrary.simpleMessage("Not implemented"),
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage(
"Notification Media Player", "Notification Media Player",
), ),
@ -102,8 +145,38 @@ class MessageLookup extends MessageLookupByLibrary {
"playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "playerSettingsDescription": MessageLookupByLibrary.simpleMessage(
"Customize the player settings", "Customize the player settings",
), ),
"playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage(
"Playback Reporting",
),
"playerSettingsPlaybackReportingIgnore":
MessageLookupByLibrary.simpleMessage(
"Ignore Playback Position Less Than",
),
"playerSettingsPlaybackReportingMinimum":
MessageLookupByLibrary.simpleMessage("Minimum Position to Report"),
"playerSettingsPlaybackReportingMinimumDescriptionHead":
MessageLookupByLibrary.simpleMessage(
"Do not report playback for the first ",
),
"playerSettingsPlaybackReportingMinimumDescriptionTail":
MessageLookupByLibrary.simpleMessage("of the book"),
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage(
"Remember Player Settings for Every Book",
),
"playerSettingsRememberForEveryBookDescription":
MessageLookupByLibrary.simpleMessage(
"Settings like speed, loudness, etc. will be remembered for every book",
),
"playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage(
"Default Speed",
),
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"Speed Options",
),
"playlistsMine": MessageLookupByLibrary.simpleMessage("My Playlists"),
"readLess": MessageLookupByLibrary.simpleMessage("Read Less"), "readLess": MessageLookupByLibrary.simpleMessage("Read Less"),
"readMore": MessageLookupByLibrary.simpleMessage("Read More"), "readMore": MessageLookupByLibrary.simpleMessage("Read More"),
"refresh": MessageLookupByLibrary.simpleMessage("Refresh"),
"reset": MessageLookupByLibrary.simpleMessage("Reset"), "reset": MessageLookupByLibrary.simpleMessage("Reset"),
"resetAppSettings": MessageLookupByLibrary.simpleMessage( "resetAppSettings": MessageLookupByLibrary.simpleMessage(
"Reset App Settings", "Reset App Settings",
@ -132,6 +205,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Restore the app settings from the backup", "Restore the app settings from the backup",
), ),
"resume": MessageLookupByLibrary.simpleMessage("Resume"), "resume": MessageLookupByLibrary.simpleMessage("Resume"),
"retry": MessageLookupByLibrary.simpleMessage("Retry"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
"shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"),
"shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage(
"Customize the shake detector settings", "Customize the shake detector settings",
@ -141,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Customize the app theme", "Customize the app theme",
), ),
"unknown": MessageLookupByLibrary.simpleMessage("Unknown"), "unknown": MessageLookupByLibrary.simpleMessage("Unknown"),
"webVersion": MessageLookupByLibrary.simpleMessage("Web Version"),
"yes": MessageLookupByLibrary.simpleMessage("Yes"), "yes": MessageLookupByLibrary.simpleMessage("Yes"),
"you": MessageLookupByLibrary.simpleMessage("You"), "you": MessageLookupByLibrary.simpleMessage("You"),
"youTooltip": MessageLookupByLibrary.simpleMessage( "youTooltip": MessageLookupByLibrary.simpleMessage(

View file

@ -24,8 +24,12 @@ class MessageLookup extends MessageLookupByLibrary {
static String m1(item) => "已删除 ${item}"; static String m1(item) => "已删除 ${item}";
static String m2(error) => "加载库时出错:${error}";
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"account": MessageLookupByLibrary.simpleMessage("账户"),
"accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"),
"appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"),
"appearance": MessageLookupByLibrary.simpleMessage("外观"), "appearance": MessageLookupByLibrary.simpleMessage("外观"),
"autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"),
@ -36,19 +40,38 @@ class MessageLookup extends MessageLookupByLibrary {
"backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"), "bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"),
"bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"), "bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"),
"bookAuthors": MessageLookupByLibrary.simpleMessage("作者"),
"bookDownloads": MessageLookupByLibrary.simpleMessage("下载"),
"bookGenres": MessageLookupByLibrary.simpleMessage("风格"),
"bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("删节版"),
"bookMetadataLength": MessageLookupByLibrary.simpleMessage("持续时间"),
"bookMetadataPublished": MessageLookupByLibrary.simpleMessage("发布年份"),
"bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage("未删节版"),
"bookSeries": MessageLookupByLibrary.simpleMessage("系列"),
"bookShelveEmpty": MessageLookupByLibrary.simpleMessage("重试"),
"bookShelveEmptyText": MessageLookupByLibrary.simpleMessage("未查询到书架"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancel": MessageLookupByLibrary.simpleMessage("取消"),
"copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"),
"copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage(
"将应用程序设置复制到剪贴板", "将应用程序设置复制到剪贴板",
), ),
"copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), "copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteDialog": m0, "deleteDialog": m0,
"deleted": m1, "deleted": m1,
"explore": MessageLookupByLibrary.simpleMessage("探索"), "explore": MessageLookupByLibrary.simpleMessage("探索"),
"exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."),
"exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"),
"general": MessageLookupByLibrary.simpleMessage("通用"), "general": MessageLookupByLibrary.simpleMessage("通用"),
"help": MessageLookupByLibrary.simpleMessage("Help"),
"home": MessageLookupByLibrary.simpleMessage("首页"), "home": MessageLookupByLibrary.simpleMessage("首页"),
"homeBookContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"),
"homeBookContinueSeries": MessageLookupByLibrary.simpleMessage("继续系列"),
"homeBookDiscover": MessageLookupByLibrary.simpleMessage("发现"),
"homeBookListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"),
"homeBookNewestAuthors": MessageLookupByLibrary.simpleMessage("最新作者"),
"homeBookRecentlyAdded": MessageLookupByLibrary.simpleMessage("最近添加"),
"homeBookRecommended": MessageLookupByLibrary.simpleMessage("推荐"),
"homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), "homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"),
"homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"),
"homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"),
@ -59,8 +82,16 @@ class MessageLookup extends MessageLookupByLibrary {
"language": MessageLookupByLibrary.simpleMessage("语言"), "language": MessageLookupByLibrary.simpleMessage("语言"),
"languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"),
"library": MessageLookupByLibrary.simpleMessage("媒体库"), "library": MessageLookupByLibrary.simpleMessage("媒体库"),
"libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"),
"libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"),
"libraryLoadError": m2,
"librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"),
"librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"),
"libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"),
"loading": MessageLookupByLibrary.simpleMessage("加载中..."),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"no": MessageLookupByLibrary.simpleMessage(""), "no": MessageLookupByLibrary.simpleMessage(""),
"notImplemented": MessageLookupByLibrary.simpleMessage("未实现"),
"notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"),
"notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage(
"在通知中自定义媒体播放器", "在通知中自定义媒体播放器",
@ -72,8 +103,32 @@ class MessageLookup extends MessageLookupByLibrary {
"playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "playerSettingsDescription": MessageLookupByLibrary.simpleMessage(
"自定义播放器设置", "自定义播放器设置",
), ),
"playerSettingsPlaybackReporting": MessageLookupByLibrary.simpleMessage(
"回放报告",
),
"playerSettingsPlaybackReportingIgnore":
MessageLookupByLibrary.simpleMessage("忽略播放位置小于"),
"playerSettingsPlaybackReportingMinimum":
MessageLookupByLibrary.simpleMessage("回放报告最小位置"),
"playerSettingsPlaybackReportingMinimumDescriptionHead":
MessageLookupByLibrary.simpleMessage("不要报告本书前 "),
"playerSettingsPlaybackReportingMinimumDescriptionTail":
MessageLookupByLibrary.simpleMessage(" 的播放"),
"playerSettingsRememberForEveryBook": MessageLookupByLibrary.simpleMessage(
"记住每本书的播放器设置",
),
"playerSettingsRememberForEveryBookDescription":
MessageLookupByLibrary.simpleMessage("每本书都会记住播放速度、音量等设置"),
"playerSettingsSpeedDefault": MessageLookupByLibrary.simpleMessage(
"默认播放速度",
),
"playerSettingsSpeedOptions": MessageLookupByLibrary.simpleMessage(
"播放速度选项",
),
"playlistsMine": MessageLookupByLibrary.simpleMessage("播放列表"),
"readLess": MessageLookupByLibrary.simpleMessage("折叠"), "readLess": MessageLookupByLibrary.simpleMessage("折叠"),
"readMore": MessageLookupByLibrary.simpleMessage("展开"), "readMore": MessageLookupByLibrary.simpleMessage("展开"),
"refresh": MessageLookupByLibrary.simpleMessage("刷新"),
"reset": MessageLookupByLibrary.simpleMessage("重置"), "reset": MessageLookupByLibrary.simpleMessage("重置"),
"resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"),
"resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage(
@ -90,6 +145,8 @@ class MessageLookup extends MessageLookupByLibrary {
"restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), "restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"),
"restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), "restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"),
"resume": MessageLookupByLibrary.simpleMessage("继续"), "resume": MessageLookupByLibrary.simpleMessage("继续"),
"retry": MessageLookupByLibrary.simpleMessage("重试"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
"shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"),
"shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage(
"自定义抖动检测器设置", "自定义抖动检测器设置",
@ -97,6 +154,7 @@ class MessageLookup extends MessageLookupByLibrary {
"themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"),
"themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), "themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"), "unknown": MessageLookupByLibrary.simpleMessage("未知"),
"webVersion": MessageLookupByLibrary.simpleMessage("Web版本"),
"yes": MessageLookupByLibrary.simpleMessage(""), "yes": MessageLookupByLibrary.simpleMessage(""),
"you": MessageLookupByLibrary.simpleMessage("我的"), "you": MessageLookupByLibrary.simpleMessage("我的"),
"youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"), "youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"),

View file

@ -74,11 +74,21 @@ class S {
return Intl.message('Cancel', name: 'cancel', desc: '', args: []); return Intl.message('Cancel', name: 'cancel', desc: '', args: []);
} }
/// `Refresh`
String get refresh {
return Intl.message('Refresh', name: 'refresh', desc: '', args: []);
}
/// `Reset` /// `Reset`
String get reset { String get reset {
return Intl.message('Reset', name: 'reset', desc: '', args: []); return Intl.message('Reset', name: 'reset', desc: '', args: []);
} }
/// `Retry`
String get retry {
return Intl.message('Retry', name: 'retry', desc: '', args: []);
}
/// `Delete` /// `Delete`
String get delete { String get delete {
return Intl.message('Delete', name: 'delete', desc: '', args: []); return Intl.message('Delete', name: 'delete', desc: '', args: []);
@ -134,6 +144,16 @@ class S {
return Intl.message('Read Less', name: 'readLess', desc: '', args: []); return Intl.message('Read Less', name: 'readLess', desc: '', args: []);
} }
/// `Loading...`
String get loading {
return Intl.message('Loading...', name: 'loading', desc: '', args: []);
}
/// `Help`
String get help {
return Intl.message('Help', name: 'help', desc: '', args: []);
}
/// `Home` /// `Home`
String get home { String get home {
return Intl.message('Home', name: 'home', desc: '', args: []); return Intl.message('Home', name: 'home', desc: '', args: []);
@ -169,6 +189,76 @@ class S {
); );
} }
/// `Continue Listening`
String get homeBookContinueListening {
return Intl.message(
'Continue Listening',
name: 'homeBookContinueListening',
desc: '',
args: [],
);
}
/// `Continue Series`
String get homeBookContinueSeries {
return Intl.message(
'Continue Series',
name: 'homeBookContinueSeries',
desc: '',
args: [],
);
}
/// `Recently Added`
String get homeBookRecentlyAdded {
return Intl.message(
'Recently Added',
name: 'homeBookRecentlyAdded',
desc: '',
args: [],
);
}
/// `Recommended`
String get homeBookRecommended {
return Intl.message(
'Recommended',
name: 'homeBookRecommended',
desc: '',
args: [],
);
}
/// `Discover`
String get homeBookDiscover {
return Intl.message(
'Discover',
name: 'homeBookDiscover',
desc: '',
args: [],
);
}
/// `Listen Again`
String get homeBookListenAgain {
return Intl.message(
'Listen Again',
name: 'homeBookListenAgain',
desc: '',
args: [],
);
}
/// `Newest Authors`
String get homeBookNewestAuthors {
return Intl.message(
'Newest Authors',
name: 'homeBookNewestAuthors',
desc: '',
args: [],
);
}
/// `About the Book` /// `About the Book`
String get bookAbout { String get bookAbout {
return Intl.message( return Intl.message(
@ -229,6 +319,46 @@ class S {
); );
} }
/// `Try again`
String get bookShelveEmpty {
return Intl.message(
'Try again',
name: 'bookShelveEmpty',
desc: '',
args: [],
);
}
/// `No shelves to display`
String get bookShelveEmptyText {
return Intl.message(
'No shelves to display',
name: 'bookShelveEmptyText',
desc: '',
args: [],
);
}
/// `Authors`
String get bookAuthors {
return Intl.message('Authors', name: 'bookAuthors', desc: '', args: []);
}
/// `Genres`
String get bookGenres {
return Intl.message('Genres', name: 'bookGenres', desc: '', args: []);
}
/// `Series`
String get bookSeries {
return Intl.message('Series', name: 'bookSeries', desc: '', args: []);
}
/// `Downloads`
String get bookDownloads {
return Intl.message('Downloads', name: 'bookDownloads', desc: '', args: []);
}
/// `Library` /// `Library`
String get library { String get library {
return Intl.message('Library', name: 'library', desc: '', args: []); return Intl.message('Library', name: 'library', desc: '', args: []);
@ -244,6 +374,56 @@ class S {
); );
} }
/// `Switch Library`
String get librarySwitchTooltip {
return Intl.message(
'Switch Library',
name: 'librarySwitchTooltip',
desc: '',
args: [],
);
}
/// `Change Library`
String get libraryChange {
return Intl.message(
'Change Library',
name: 'libraryChange',
desc: '',
args: [],
);
}
/// `Select Library`
String get librarySelect {
return Intl.message(
'Select Library',
name: 'librarySelect',
desc: '',
args: [],
);
}
/// `No libraries available.`
String get libraryEmpty {
return Intl.message(
'No libraries available.',
name: 'libraryEmpty',
desc: '',
args: [],
);
}
/// `Error loading libraries: {error}`
String libraryLoadError(String error) {
return Intl.message(
'Error loading libraries: $error',
name: 'libraryLoadError',
desc: '',
args: [error],
);
}
/// `explore` /// `explore`
String get explore { String get explore {
return Intl.message('explore', name: 'explore', desc: '', args: []); return Intl.message('explore', name: 'explore', desc: '', args: []);
@ -259,6 +439,16 @@ class S {
); );
} }
/// `Seek and you shall discover...`
String get exploreHint {
return Intl.message(
'Seek and you shall discover...',
name: 'exploreHint',
desc: '',
args: [],
);
}
/// `You` /// `You`
String get you { String get you {
return Intl.message('You', name: 'you', desc: '', args: []); return Intl.message('You', name: 'you', desc: '', args: []);
@ -274,6 +464,41 @@ class S {
); );
} }
/// `Settings`
String get settings {
return Intl.message('Settings', name: 'settings', desc: '', args: []);
}
/// `Account`
String get account {
return Intl.message('Account', name: 'account', desc: '', args: []);
}
/// `Switch Account`
String get accountSwitch {
return Intl.message(
'Switch Account',
name: 'accountSwitch',
desc: '',
args: [],
);
}
/// `My Playlists`
String get playlistsMine {
return Intl.message(
'My Playlists',
name: 'playlistsMine',
desc: '',
args: [],
);
}
/// `Web Version`
String get webVersion {
return Intl.message('Web Version', name: 'webVersion', desc: '', args: []);
}
/// `App Settings` /// `App Settings`
String get appSettings { String get appSettings {
return Intl.message( return Intl.message(
@ -324,6 +549,96 @@ class S {
); );
} }
/// `Remember Player Settings for Every Book`
String get playerSettingsRememberForEveryBook {
return Intl.message(
'Remember Player Settings for Every Book',
name: 'playerSettingsRememberForEveryBook',
desc: '',
args: [],
);
}
/// `Settings like speed, loudness, etc. will be remembered for every book`
String get playerSettingsRememberForEveryBookDescription {
return Intl.message(
'Settings like speed, loudness, etc. will be remembered for every book',
name: 'playerSettingsRememberForEveryBookDescription',
desc: '',
args: [],
);
}
/// `Default Speed`
String get playerSettingsSpeedDefault {
return Intl.message(
'Default Speed',
name: 'playerSettingsSpeedDefault',
desc: '',
args: [],
);
}
/// `Speed Options`
String get playerSettingsSpeedOptions {
return Intl.message(
'Speed Options',
name: 'playerSettingsSpeedOptions',
desc: '',
args: [],
);
}
/// `Playback Reporting`
String get playerSettingsPlaybackReporting {
return Intl.message(
'Playback Reporting',
name: 'playerSettingsPlaybackReporting',
desc: '',
args: [],
);
}
/// `Minimum Position to Report`
String get playerSettingsPlaybackReportingMinimum {
return Intl.message(
'Minimum Position to Report',
name: 'playerSettingsPlaybackReportingMinimum',
desc: '',
args: [],
);
}
/// `Do not report playback for the first `
String get playerSettingsPlaybackReportingMinimumDescriptionHead {
return Intl.message(
'Do not report playback for the first ',
name: 'playerSettingsPlaybackReportingMinimumDescriptionHead',
desc: '',
args: [],
);
}
/// `of the book`
String get playerSettingsPlaybackReportingMinimumDescriptionTail {
return Intl.message(
'of the book',
name: 'playerSettingsPlaybackReportingMinimumDescriptionTail',
desc: '',
args: [],
);
}
/// `Ignore Playback Position Less Than`
String get playerSettingsPlaybackReportingIgnore {
return Intl.message(
'Ignore Playback Position Less Than',
name: 'playerSettingsPlaybackReportingIgnore',
desc: '',
args: [],
);
}
/// `Auto Turn On Sleep Timer` /// `Auto Turn On Sleep Timer`
String get autoTurnOnSleepTimer { String get autoTurnOnSleepTimer {
return Intl.message( return Intl.message(
@ -568,6 +883,21 @@ class S {
args: [], args: [],
); );
} }
/// `Logs`
String get logs {
return Intl.message('Logs', name: 'logs', desc: '', args: []);
}
/// `Not implemented`
String get notImplemented {
return Intl.message(
'Not implemented',
name: 'notImplemented',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -5,7 +5,9 @@
"no": "No", "no": "No",
"ok": "OK", "ok": "OK",
"cancel": "Cancel", "cancel": "Cancel",
"refresh": "Refresh",
"reset": "Reset", "reset": "Reset",
"retry": "Retry",
"delete": "Delete", "delete": "Delete",
"deleteDialog": "Are you sure you want to delete {item}?", "deleteDialog": "Are you sure you want to delete {item}?",
"@deleteDialog": { "@deleteDialog": {
@ -31,31 +33,74 @@
"unknown": "Unknown", "unknown": "Unknown",
"readMore": "Read More", "readMore": "Read More",
"readLess": "Read Less", "readLess": "Read Less",
"loading": "Loading...",
"help": "Help",
"home": "Home", "home": "Home",
"homeListenAgain": "Listen Again", "homeListenAgain": "Listen Again",
"homeContinueListening": "Continue Listening", "homeContinueListening": "Continue Listening",
"homeStartListening": "Start Listening", "homeStartListening": "Start Listening",
"homeBookContinueListening": "Continue Listening",
"homeBookContinueSeries": "Continue Series",
"homeBookRecentlyAdded": "Recently Added",
"homeBookRecommended": "Recommended",
"homeBookDiscover": "Discover",
"homeBookListenAgain": "Listen Again",
"homeBookNewestAuthors": "Newest Authors",
"bookAbout": "About the Book", "bookAbout": "About the Book",
"bookAboutDefault": "Sorry, no description found", "bookAboutDefault": "Sorry, no description found",
"bookMetadataAbridged": "Abridged", "bookMetadataAbridged": "Abridged",
"bookMetadataUnabridged": "Unabridged", "bookMetadataUnabridged": "Unabridged",
"bookMetadataLength": "Length", "bookMetadataLength": "Length",
"bookMetadataPublished": "Published", "bookMetadataPublished": "Published",
"bookShelveEmpty": "Try again",
"bookShelveEmptyText": "No shelves to display",
"bookAuthors": "Authors",
"bookGenres": "Genres",
"bookSeries": "Series",
"bookDownloads": "Downloads",
"library": "Library", "library": "Library",
"libraryTooltip": "Browse your library", "libraryTooltip": "Browse your library",
"librarySwitchTooltip": "Switch Library",
"libraryChange": "Change Library",
"librarySelect": "Select Library",
"libraryEmpty": "No libraries available.",
"libraryLoadError": "Error loading libraries: {error}",
"@libraryLoadError": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"explore": "explore", "explore": "explore",
"exploreTooltip": "Search and Explore", "exploreTooltip": "Search and Explore",
"exploreHint": "Seek and you shall discover...",
"you": "You", "you": "You",
"youTooltip": "Your Profile and Settings", "youTooltip": "Your Profile and Settings",
"settings": "Settings",
"account": "Account",
"accountSwitch": "Switch Account",
"playlistsMine": "My Playlists",
"webVersion": "Web Version",
"appSettings": "App Settings", "appSettings": "App Settings",
"general": "General", "general": "General",
"language": "Language", "language": "Language",
"languageDescription": "Language switch", "languageDescription": "Language switch",
"playerSettings": "Player Settings", "playerSettings": "Player Settings",
"playerSettingsDescription": "Customize the player settings", "playerSettingsDescription": "Customize the player settings",
"playerSettingsRememberForEveryBook": "Remember Player Settings for Every Book",
"playerSettingsRememberForEveryBookDescription": "Settings like speed, loudness, etc. will be remembered for every book",
"playerSettingsSpeedDefault": "Default Speed",
"playerSettingsSpeedOptions": "Speed Options",
"playerSettingsPlaybackReporting": "Playback Reporting",
"playerSettingsPlaybackReportingMinimum": "Minimum Position to Report",
"playerSettingsPlaybackReportingMinimumDescriptionHead": "Do not report playback for the first ",
"playerSettingsPlaybackReportingMinimumDescriptionTail": "of the book",
"playerSettingsPlaybackReportingIgnore": "Ignore Playback Position Less Than",
"autoTurnOnSleepTimer": "Auto Turn On Sleep Timer", "autoTurnOnSleepTimer": "Auto Turn On Sleep Timer",
"automaticallyDescription": "Automatically turn on the sleep timer based on the time of day", "automaticallyDescription": "Automatically turn on the sleep timer based on the time of day",
"shakeDetector": "Shake Detector", "shakeDetector": "Shake Detector",
@ -82,6 +127,11 @@
"resetAppSettings": "Reset App Settings", "resetAppSettings": "Reset App Settings",
"resetAppSettingsDescription": "Reset the app settings to the default values", "resetAppSettingsDescription": "Reset the app settings to the default values",
"resetAppSettingsDialog": "Are you sure you want to reset the app settings?" "resetAppSettingsDialog": "Are you sure you want to reset the app settings?",
"logs": "Logs",
"notImplemented": "Not implemented"
} }

View file

@ -5,8 +5,10 @@
"no": "否", "no": "否",
"ok": "确定", "ok": "确定",
"cancel": "取消", "cancel": "取消",
"refresh": "刷新",
"reset": "重置", "reset": "重置",
"delete": "Delete", "retry": "重试",
"delete": "删除",
"deleteDialog": "确定要删除 {item} 吗?", "deleteDialog": "确定要删除 {item} 吗?",
"@deleteDialog": { "@deleteDialog": {
"placeholders": { "placeholders": {
@ -31,30 +33,76 @@
"unknown": "未知", "unknown": "未知",
"readMore": "展开", "readMore": "展开",
"readLess": "折叠", "readLess": "折叠",
"loading": "加载中...",
"help": "Help",
"home": "首页", "home": "首页",
"homeListenAgain": "再听一遍", "homeListenAgain": "再听一遍",
"homeContinueListening": "继续收听", "homeContinueListening": "继续收听",
"homeStartListening": "开始收听", "homeStartListening": "开始收听",
"homeBookContinueListening": "继续收听",
"homeBookContinueSeries": "继续系列",
"homeBookRecentlyAdded": "最近添加",
"homeBookRecommended": "推荐",
"homeBookDiscover": "发现",
"homeBookListenAgain": "再听一遍",
"homeBookNewestAuthors": "最新作者",
"bookAbout": "关于本书", "bookAbout": "关于本书",
"bookAboutDefault": "抱歉,找不到描述", "bookAboutDefault": "抱歉,找不到描述",
"bookMetadataAbridged": "删节版",
"bookMetadataUnabridged": "未删节版",
"bookMetadataLength": "持续时间",
"bookMetadataPublished": "发布年份",
"bookShelveEmpty": "重试",
"bookShelveEmptyText": "未查询到书架",
"bookAuthors": "作者",
"bookGenres": "风格",
"bookSeries": "系列",
"bookDownloads": "下载",
"library": "媒体库", "library": "媒体库",
"libraryTooltip": "浏览您的媒体库", "libraryTooltip": "浏览您的媒体库",
"librarySwitchTooltip": "切换媒体库",
"libraryChange": "更改媒体库",
"librarySelect": "选择媒体库",
"libraryEmpty": "没有可用的库。",
"libraryLoadError": "加载库时出错:{error}",
"@libraryLoadError": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"explore": "探索", "explore": "探索",
"exploreTooltip": "搜索和探索", "exploreTooltip": "搜索和探索",
"exploreHint": "搜索与探索...",
"you": "我的", "you": "我的",
"youTooltip": "您的个人资料和设置", "youTooltip": "您的个人资料和设置",
"settings": "设置",
"account": "账户",
"accountSwitch": "切换账户",
"playlistsMine": "播放列表",
"webVersion": "Web版本",
"appSettings": "应用设置", "appSettings": "应用设置",
"general": "通用", "general": "通用",
"language": "语言", "language": "语言",
"languageDescription": "语言切换", "languageDescription": "语言切换",
"playerSettings": "播放器设置", "playerSettings": "播放器设置",
"playerSettingsDescription": "自定义播放器设置", "playerSettingsDescription": "自定义播放器设置",
"playerSettingsRememberForEveryBook": "记住每本书的播放器设置",
"playerSettingsRememberForEveryBookDescription": "每本书都会记住播放速度、音量等设置",
"playerSettingsSpeedDefault": "默认播放速度",
"playerSettingsSpeedOptions": "播放速度选项",
"playerSettingsPlaybackReporting": "回放报告",
"playerSettingsPlaybackReportingMinimum": "回放报告最小位置",
"playerSettingsPlaybackReportingMinimumDescriptionHead": "不要报告本书前 ",
"playerSettingsPlaybackReportingMinimumDescriptionTail": " 的播放",
"playerSettingsPlaybackReportingIgnore": "忽略播放位置小于",
"autoTurnOnSleepTimer": "自动开启睡眠定时器", "autoTurnOnSleepTimer": "自动开启睡眠定时器",
"automaticallyDescription": "根据一天中的时间自动打开睡眠定时器", "automaticallyDescription": "根据一天中的时间自动打开睡眠定时器",
"shakeDetector": "抖动检测器", "shakeDetector": "抖动检测器",
"shakeDetectorDescription": "自定义抖动检测器设置", "shakeDetectorDescription": "自定义抖动检测器设置",
"appearance": "外观", "appearance": "外观",
@ -79,6 +127,8 @@
"resetAppSettings": "重置应用程序设置", "resetAppSettings": "重置应用程序设置",
"resetAppSettingsDescription": "将应用程序设置重置为默认值", "resetAppSettingsDescription": "将应用程序设置重置为默认值",
"resetAppSettingsDialog": "您确定要重置应用程序设置吗?" "resetAppSettingsDialog": "您确定要重置应用程序设置吗?",
"logs": "日志",
"notImplemented": "未实现"
} }

View file

@ -12,16 +12,18 @@ import 'package:vaani/features/player/core/init.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart' import 'package:vaani/features/player/providers/audiobook_player.dart'
show audiobookPlayerProvider, simpleAudiobookPlayerProvider; show audiobookPlayerProvider, simpleAudiobookPlayerProvider;
import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/features/shake_detection/providers/shake_detector.dart';
import 'package:vaani/features/skip_start_end/skip_start_end_provider.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/settings.dart';
import 'package:vaani/theme/providers/system_theme_provider.dart'; import 'package:vaani/theme/providers/system_theme_provider.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
import 'package:vaani/theme/theme.dart'; import 'package:vaani/theme/theme.dart';
final appLogger = Logger('vaani'); final appLogger = Logger(AppMetadata.appName);
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -171,6 +173,7 @@ class _EagerInitialization extends ConsumerWidget {
ref.watch(playbackReporterProvider); ref.watch(playbackReporterProvider);
ref.watch(simpleDownloadManagerProvider); ref.watch(simpleDownloadManagerProvider);
ref.watch(shakeDetectorProvider); ref.watch(shakeDetectorProvider);
ref.watch(skipStartEndProvider);
} catch (e) { } catch (e) {
debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
appLogger.severe(e.toString()); appLogger.severe(e.toString());

View file

@ -3,11 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/main.dart'; import 'package:vaani/main.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/app_settings_provider.dart' import 'package:vaani/settings/app_settings_provider.dart' show appSettingsProvider;
show appSettingsProvider; import 'package:vaani/settings/constants.dart';
import '../shared/widgets/shelves/home_shelf.dart'; import '../shared/widgets/shelves/home_shelf.dart';
@ -21,11 +22,12 @@ class HomePage extends HookConsumerWidget {
final scrollController = useScrollController(); final scrollController = useScrollController();
final appSettings = ref.watch(appSettingsProvider); final appSettings = ref.watch(appSettingsProvider);
final homePageSettings = appSettings.homePageSettings; final homePageSettings = appSettings.homePageSettings;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: GestureDetector( title: GestureDetector(
child: Text( child: Text(
'Vaani', AppMetadata.appName,
style: Theme.of(context).textTheme.headlineLarge, style: Theme.of(context).textTheme.headlineLarge,
), ),
onTap: () { onTap: () {
@ -48,7 +50,7 @@ class HomePage extends HookConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text('No shelves to display'), Text(S.of(context).bookShelveEmptyText),
// try again button // try again button
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
@ -57,7 +59,7 @@ class HomePage extends HookConsumerWidget {
); );
ref.invalidate(personalizedViewProvider); ref.invalidate(personalizedViewProvider);
}, },
child: const Text('Try again'), child: Text(S.of(context).bookShelveEmpty),
), ),
], ],
), ),
@ -70,16 +72,23 @@ class HomePage extends HookConsumerWidget {
// check if showPlayButton is enabled for the shelf // check if showPlayButton is enabled for the shelf
// using the id of the shelf // using the id of the shelf
final showPlayButton = switch (shelf.id) { final showPlayButton = switch (shelf.id) {
'continue-listening' => 'continue-listening' => homePageSettings.showPlayButtonOnContinueListeningShelf,
homePageSettings.showPlayButtonOnContinueListeningShelf, 'continue-series' => homePageSettings.showPlayButtonOnContinueSeriesShelf,
'continue-series' => 'listen-again' => homePageSettings.showPlayButtonOnListenAgainShelf,
homePageSettings.showPlayButtonOnContinueSeriesShelf,
'listen-again' =>
homePageSettings.showPlayButtonOnListenAgainShelf,
_ => homePageSettings.showPlayButtonOnAllRemainingShelves, _ => homePageSettings.showPlayButtonOnAllRemainingShelves,
}; };
final showLabel = switch (shelf.label) {
"Continue Listening" => S.of(context).homeBookContinueListening,
"Continue Series" => S.of(context).homeBookContinueSeries,
"Recently Added" => S.of(context).homeBookRecentlyAdded,
"Recommended" => S.of(context).homeBookRecommended,
"Discover" => S.of(context).homeBookDiscover,
"Listen Again" => S.of(context).homeBookListenAgain,
"Newest Authors" => S.of(context).homeBookNewestAuthors,
_ => shelf.label
};
return HomeShelf( return HomeShelf(
title: shelf.label, title: showLabel,
shelf: shelf, shelf: shelf,
showPlayButton: showPlayButton, showPlayButton: showPlayButton,
); );
@ -102,8 +111,7 @@ class HomePage extends HookConsumerWidget {
}, },
loading: () => const HomePageSkeleton(), loading: () => const HomePageSkeleton(),
error: (error, stack) { error: (error, stack) {
if (apiSettings.activeUser == null || if (apiSettings.activeUser == null || apiSettings.activeServer == null) {
apiSettings.activeServer == null) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/view/buttons.dart'; import 'package:vaani/settings/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/settings/view/simple_settings_page.dart';
@ -20,7 +21,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
final primaryColor = Theme.of(context).colorScheme.primary; final primaryColor = Theme.of(context).colorScheme.primary;
return SimpleSettingsPage( return SimpleSettingsPage(
title: const Text('Player Settings'), title: Text(S.of(context).playerSettings),
sections: [ sections: [
SettingsSection( SettingsSection(
margin: const EdgeInsetsDirectional.symmetric( margin: const EdgeInsetsDirectional.symmetric(
@ -30,10 +31,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
tiles: [ tiles: [
// preferred settings for every book // preferred settings for every book
SettingsTile.switchTile( SettingsTile.switchTile(
title: const Text('Remember Player Settings for Every Book'), title: Text(S.of(context).playerSettingsRememberForEveryBook),
leading: const Icon(Icons.settings_applications), leading: const Icon(Icons.settings_applications),
description: const Text( description: Text(
'Settings like speed, loudness, etc. will be remembered for every book', S.of(context).playerSettingsRememberForEveryBookDescription,
), ),
initialValue: playerSettings.configurePlayerForEveryBook, initialValue: playerSettings.configurePlayerForEveryBook,
onToggle: (value) { onToggle: (value) {
@ -47,11 +48,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
// preferred default speed // preferred default speed
SettingsTile( SettingsTile(
title: const Text('Default Speed'), title: Text(S.of(context).playerSettingsSpeedDefault),
trailing: Text( trailing: Text(
'${playerSettings.preferredDefaultSpeed}x', '${playerSettings.preferredDefaultSpeed}x',
style: style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
), ),
leading: const Icon(Icons.speed), leading: const Icon(Icons.speed),
onPressed: (context) async { onPressed: (context) async {
@ -72,11 +72,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
), ),
// preferred speed options // preferred speed options
SettingsTile( SettingsTile(
title: const Text('Speed Options'), title: Text(S.of(context).playerSettingsSpeedOptions),
description: Text( description: Text(
playerSettings.speedOptions.map((e) => '${e}x').join(', '), playerSettings.speedOptions.map((e) => '${e}x').join(', '),
style: style: TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
), ),
leading: const Icon(Icons.speed), leading: const Icon(Icons.speed),
onPressed: (context) async { onPressed: (context) async {
@ -100,23 +99,23 @@ class PlayerSettingsPage extends HookConsumerWidget {
// Playback Reporting // Playback Reporting
SettingsSection( SettingsSection(
title: const Text('Playback Reporting'), title: Text(S.of(context).playerSettingsPlaybackReporting),
tiles: [ tiles: [
SettingsTile( SettingsTile(
title: const Text('Minimum Position to Report'), title: Text(S.of(context).playerSettingsPlaybackReportingMinimum),
description: Text.rich( description: Text.rich(
TextSpan( TextSpan(
text: 'Do not report playback for the first ', text: S.of(context).playerSettingsPlaybackReportingMinimumDescriptionHead,
children: [ children: [
TextSpan( TextSpan(
text: playerSettings text: playerSettings.minimumPositionForReporting.smartBinaryFormat,
.minimumPositionForReporting.smartBinaryFormat,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: primaryColor, color: primaryColor,
), ),
), ),
const TextSpan(text: ' of the book'), TextSpan(
text: S.of(context).playerSettingsPlaybackReportingMinimumDescriptionTail),
], ],
), ),
), ),
@ -126,7 +125,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
context: context, context: context,
builder: (context) { builder: (context) {
return TimeDurationSelector( return TimeDurationSelector(
title: const Text('Ignore Playback Position Less Than'), title: Text(S.of(context).playerSettingsPlaybackReportingIgnore),
baseUnit: BaseUnit.second, baseUnit: BaseUnit.second,
initialValue: playerSettings.minimumPositionForReporting, initialValue: playerSettings.minimumPositionForReporting,
); );
@ -149,8 +148,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
text: 'Mark complete when less than ', text: 'Mark complete when less than ',
children: [ children: [
TextSpan( TextSpan(
text: playerSettings text: playerSettings.markCompleteWhenTimeLeft.smartBinaryFormat,
.markCompleteWhenTimeLeft.smartBinaryFormat,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: primaryColor, color: primaryColor,
@ -189,8 +187,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
text: 'Report progress every ', text: 'Report progress every ',
children: [ children: [
TextSpan( TextSpan(
text: playerSettings text: playerSettings.playbackReportInterval.smartBinaryFormat,
.playbackReportInterval.smartBinaryFormat,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: primaryColor, color: primaryColor,
@ -234,8 +231,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
description: const Text( description: const Text(
'Show the total progress of the book in the player', 'Show the total progress of the book in the player',
), ),
initialValue: initialValue: playerSettings.expandedPlayerSettings.showTotalProgress,
playerSettings.expandedPlayerSettings.showTotalProgress,
onToggle: (value) { onToggle: (value) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings appSettings.copyWith.playerSettings
@ -250,13 +246,11 @@ class PlayerSettingsPage extends HookConsumerWidget {
description: const Text( description: const Text(
'Show the progress of the current chapter in the player', 'Show the progress of the current chapter in the player',
), ),
initialValue: initialValue: playerSettings.expandedPlayerSettings.showChapterProgress,
playerSettings.expandedPlayerSettings.showChapterProgress,
onToggle: (value) { onToggle: (value) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings( appSettings.copyWith.playerSettings(
expandedPlayerSettings: playerSettings expandedPlayerSettings: playerSettings.expandedPlayerSettings
.expandedPlayerSettings
.copyWith(showChapterProgress: value), .copyWith(showChapterProgress: value),
), ),
); );
@ -315,8 +309,7 @@ class SpeedPicker extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final speedController = final speedController = useTextEditingController(text: initialValue.toString());
useTextEditingController(text: initialValue.toString());
final speed = useState<double?>(initialValue); final speed = useState<double?>(initialValue);
return AlertDialog( return AlertDialog(
title: const Text('Select Speed'), title: const Text('Select Speed'),
@ -375,8 +368,7 @@ class SpeedOptionsPicker extends HookConsumerWidget {
onDeleted: speed == 1 onDeleted: speed == 1
? null ? null
: () { : () {
speedOptions.value = speedOptions.value = speedOptions.value.where((element) {
speedOptions.value.where((element) {
// speed option 1 can't be removed // speed option 1 can't be removed
return element != speed; return element != speed;
}).toList(); }).toList();

View file

@ -0,0 +1,6 @@
extension StringExtension on String {
String get capitalize {
if (isEmpty) return "";
return "${this[0].toUpperCase()}${substring(1)}";
}
}

View file

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:vaani/generated/l10n.dart';
void showNotImplementedToast(BuildContext context) { void showNotImplementedToast(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text("Not implemented"), content: Text(S.of(context).notImplemented),
showCloseIcon: true, showCloseIcon: true,
), ),
); );