From 620a1eb7a2837bb59da5cca9f31119152e523d0e Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Fri, 24 Oct 2025 11:47:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B7=B3=E8=BF=87=E7=89=87?= =?UTF-8?q?=E5=A4=B4=E7=89=87=E5=B0=BE,=E4=B8=8A=E4=B8=80=E7=AB=A0?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E7=AB=A0=E7=A7=BB=E5=8A=A8=E5=88=B0AudioPlay?= =?UTF-8?q?er=E5=AF=B9=E8=B1=A1=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../downloads/view/downloads_page.dart | 3 +- lib/features/explore/view/explore_page.dart | 21 +- .../view/library_item_actions.dart | 2 + .../view/library_browser_page.dart | 19 +- .../models/nullable_player_settings.dart | 2 + .../nullable_player_settings.freezed.dart | 60 +++- .../models/nullable_player_settings.g.dart | 8 + .../providers/book_settings_provider.dart | 4 +- .../providers/book_settings_provider.g.dart | 2 +- .../player/core/audiobook_player.dart | 74 +++- .../player/view/player_when_expanded.dart | 4 + .../audiobook_player_seek_chapter_button.dart | 70 ++-- .../widgets/chapter_selection_button.dart | 20 +- .../player_skip_chapter_start_end.dart | 99 ++++++ .../skip_start_end/skip_start_end.dart | 86 +++++ .../skip_start_end_provider.dart | 23 ++ .../skip_start_end_provider.g.dart | 25 ++ .../you/view/widgets/library_switch_chip.dart | 29 +- lib/features/you/view/you_page.dart | 28 +- lib/generated/intl/messages_en.dart | 76 ++++ lib/generated/intl/messages_zh.dart | 60 +++- lib/generated/l10n.dart | 330 ++++++++++++++++++ lib/l10n/intl_en.arb | 52 ++- lib/l10n/intl_zh.arb | 56 ++- lib/main.dart | 5 +- lib/pages/home_page.dart | 36 +- lib/settings/view/player_settings_page.dart | 54 ++- lib/shared/extensions/string.dart | 6 + lib/shared/widgets/not_implemented.dart | 5 +- 29 files changed, 1080 insertions(+), 179 deletions(-) create mode 100644 lib/features/skip_start_end/player_skip_chapter_start_end.dart create mode 100644 lib/features/skip_start_end/skip_start_end.dart create mode 100644 lib/features/skip_start_end/skip_start_end_provider.dart create mode 100644 lib/features/skip_start_end/skip_start_end_provider.g.dart create mode 100644 lib/shared/extensions/string.dart diff --git a/lib/features/downloads/view/downloads_page.dart b/lib/features/downloads/view/downloads_page.dart index e05c391..95a19b1 100644 --- a/lib/features/downloads/view/downloads_page.dart +++ b/lib/features/downloads/view/downloads_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; +import 'package:vaani/generated/l10n.dart'; class DownloadsPage extends HookConsumerWidget { const DownloadsPage({super.key}); @@ -13,7 +14,7 @@ class DownloadsPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Downloads'), + title: Text(S.of(context).bookDownloads), ), body: Center( // history of downloads diff --git a/lib/features/explore/view/explore_page.dart b/lib/features/explore/view/explore_page.dart index 1682254..4060931 100644 --- a/lib/features/explore/view/explore_page.dart +++ b/lib/features/explore/view/explore_page.dart @@ -11,6 +11,7 @@ import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/explore/providers/search_controller.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/settings/api_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart'; @@ -29,7 +30,7 @@ class ExplorePage extends HookConsumerWidget { final api = ref.watch(authenticatedApiProvider); return Scaffold( appBar: AppBar( - title: const Text('Explore'), + title: Text(S.of(context).explore), ), body: const MySearchBar(), ); @@ -61,8 +62,8 @@ class MySearchBar extends HookConsumerWidget { currentQuery = query; // In a real application, there should be some error handling here. - final options = await api.libraries - .search(libraryId: settings.activeLibraryId!, query: query, limit: 3); + final options = + await api.libraries.search(libraryId: settings.activeLibraryId!, query: query, limit: 3); // If another search happened after this one, throw away these options. if (currentQuery != query) { @@ -93,14 +94,11 @@ class MySearchBar extends HookConsumerWidget { // "Seek and you shall find... your next book!" // "Let's uncover your next favorite book..." // "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 hintStyle: WidgetStatePropertyAll( Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5), ), ), textInputAction: TextInputAction.search, @@ -137,7 +135,7 @@ class MySearchBar extends HookConsumerWidget { // debugPrint('options: $options'); if (options == null) { // TODO: show loading indicator or failure message - return [const ListTile(title: Text('Loading...'))]; + return [ListTile(title: Text(S.of(context).loading))]; } // see if BookLibrarySearchResponse or PodcastLibrarySearchResponse if (options is BookLibrarySearchResponse) { @@ -233,9 +231,8 @@ class BookSearchResultMini extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull; - final image = item == null - ? const AsyncValue.loading() - : ref.watch(coverImageProvider(item.id)); + final image = + item == null ? const AsyncValue.loading() : ref.watch(coverImageProvider(item.id)); return ListTile( leading: SizedBox( width: 50, diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 1cf596c..394a2e7 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -509,6 +509,7 @@ Future libraryItemPlayButtonOnPressed({ }) async { appLogger.info('Pressed play/resume button'); final player = ref.watch(audiobookPlayerProvider); + // final bookSettings = ref.watch(bookSettingsProvider(book.libraryItemId)); final isCurrentBookSetInPlayer = player.book == book; final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; @@ -554,6 +555,7 @@ Future libraryItemPlayButtonOnPressed({ ? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed : appPlayerSettings.preferredDefaultSpeed, ), + // player.setClip(start: Duration(seconds: 10)), ]); // toggle play/pause diff --git a/lib/features/library_browser/view/library_browser_page.dart b/lib/features/library_browser/view/library_browser_page.dart index 4327b17..c8cb286 100644 --- a/lib/features/library_browser/view/library_browser_page.dart +++ b/lib/features/library_browser/view/library_browser_page.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; -import 'package:vaani/features/you/view/widgets/library_switch_chip.dart' - show showLibrarySwitcher; +import 'package:vaani/features/you/view/widgets/library_switch_chip.dart' show showLibrarySwitcher; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/router.dart' show Routes; import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; -import 'package:vaani/shared/widgets/not_implemented.dart' - show showNotImplementedToast; +import 'package:vaani/shared/widgets/not_implemented.dart' show showNotImplementedToast; class LibraryBrowserPage extends HookConsumerWidget { const LibraryBrowserPage({super.key}); @@ -20,7 +19,7 @@ class LibraryBrowserPage extends HookConsumerWidget { AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books; // Determine the title text - final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library'; + final String appBarTitle = currentLibrary?.name ?? S.of(context).library; return Scaffold( // 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) leading: IconButton( icon: Icon(libraryIconData), - tooltip: 'Switch Library', // Helpful tooltip for users + tooltip: S.of(context).librarySwitchTooltip, // Helpful tooltip for users onPressed: () { showLibrarySwitcher(context, ref); }, @@ -44,7 +43,7 @@ class LibraryBrowserPage extends HookConsumerWidget { delegate: SliverChildListDelegate( [ ListTile( - title: const Text('Authors'), + title: Text(S.of(context).bookAuthors), leading: const Icon(Icons.person), trailing: const Icon(Icons.chevron_right), onTap: () { @@ -52,7 +51,7 @@ class LibraryBrowserPage extends HookConsumerWidget { }, ), ListTile( - title: const Text('Genres'), + title: Text(S.of(context).bookGenres), leading: const Icon(Icons.category), trailing: const Icon(Icons.chevron_right), onTap: () { @@ -60,7 +59,7 @@ class LibraryBrowserPage extends HookConsumerWidget { }, ), ListTile( - title: const Text('Series'), + title: Text(S.of(context).bookSeries), leading: const Icon(Icons.list), trailing: const Icon(Icons.chevron_right), onTap: () { @@ -69,7 +68,7 @@ class LibraryBrowserPage extends HookConsumerWidget { ), // Downloads ListTile( - title: const Text('Downloads'), + title: Text(S.of(context).bookDownloads), leading: const Icon(Icons.download), trailing: const Icon(Icons.chevron_right), onTap: () { diff --git a/lib/features/per_book_settings/models/nullable_player_settings.dart b/lib/features/per_book_settings/models/nullable_player_settings.dart index 39135d7..4f933e5 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.dart @@ -14,6 +14,8 @@ class NullablePlayerSettings with _$NullablePlayerSettings { List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval, + @Default(Duration()) Duration skipChapterStart, + @Default(Duration()) Duration skipChapterEnd, }) = _NullablePlayerSettings; factory NullablePlayerSettings.fromJson(Map json) => diff --git a/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart b/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart index 9450ed4..44aacd5 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart @@ -31,6 +31,8 @@ mixin _$NullablePlayerSettings { SleepTimerSettings? get sleepTimerSettings => throw _privateConstructorUsedError; Duration? get playbackReportInterval => throw _privateConstructorUsedError; + Duration get skipChapterStart => throw _privateConstructorUsedError; + Duration get skipChapterEnd => throw _privateConstructorUsedError; /// Serializes this NullablePlayerSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -55,7 +57,9 @@ abstract class $NullablePlayerSettingsCopyWith<$Res> { double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, - Duration? playbackReportInterval}); + Duration? playbackReportInterval, + Duration skipChapterStart, + Duration skipChapterEnd}); $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings; @@ -85,6 +89,8 @@ class _$NullablePlayerSettingsCopyWithImpl<$Res, Object? speedOptions = freezed, Object? sleepTimerSettings = freezed, Object? playbackReportInterval = freezed, + Object? skipChapterStart = null, + Object? skipChapterEnd = null, }) { return _then(_value.copyWith( miniPlayerSettings: freezed == miniPlayerSettings @@ -115,6 +121,14 @@ class _$NullablePlayerSettingsCopyWithImpl<$Res, ? _value.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable 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); } @@ -180,7 +194,9 @@ abstract class _$$NullablePlayerSettingsImplCopyWith<$Res> double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, - Duration? playbackReportInterval}); + Duration? playbackReportInterval, + Duration skipChapterStart, + Duration skipChapterEnd}); @override $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; @@ -212,6 +228,8 @@ class __$$NullablePlayerSettingsImplCopyWithImpl<$Res> Object? speedOptions = freezed, Object? sleepTimerSettings = freezed, Object? playbackReportInterval = freezed, + Object? skipChapterStart = null, + Object? skipChapterEnd = null, }) { return _then(_$NullablePlayerSettingsImpl( miniPlayerSettings: freezed == miniPlayerSettings @@ -242,6 +260,14 @@ class __$$NullablePlayerSettingsImplCopyWithImpl<$Res> ? _value.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable 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, final List? speedOptions, this.sleepTimerSettings, - this.playbackReportInterval}) + this.playbackReportInterval, + this.skipChapterStart = const Duration(), + this.skipChapterEnd = const Duration()}) : _speedOptions = speedOptions; factory _$NullablePlayerSettingsImpl.fromJson(Map json) => @@ -284,10 +312,16 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings { final SleepTimerSettings? sleepTimerSettings; @override final Duration? playbackReportInterval; + @override + @JsonKey() + final Duration skipChapterStart; + @override + @JsonKey() + final Duration skipChapterEnd; @override 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 @@ -308,7 +342,11 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings { (identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings) && (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) @@ -321,7 +359,9 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings { preferredDefaultSpeed, const DeepCollectionEquality().hash(_speedOptions), sleepTimerSettings, - playbackReportInterval); + playbackReportInterval, + skipChapterStart, + skipChapterEnd); /// Create a copy of NullablePlayerSettings /// with the given fields replaced by the non-null parameter values. @@ -348,7 +388,9 @@ abstract class _NullablePlayerSettings implements NullablePlayerSettings { final double? preferredDefaultSpeed, final List? speedOptions, final SleepTimerSettings? sleepTimerSettings, - final Duration? playbackReportInterval}) = _$NullablePlayerSettingsImpl; + final Duration? playbackReportInterval, + final Duration skipChapterStart, + final Duration skipChapterEnd}) = _$NullablePlayerSettingsImpl; factory _NullablePlayerSettings.fromJson(Map json) = _$NullablePlayerSettingsImpl.fromJson; @@ -367,6 +409,10 @@ abstract class _NullablePlayerSettings implements NullablePlayerSettings { SleepTimerSettings? get sleepTimerSettings; @override Duration? get playbackReportInterval; + @override + Duration get skipChapterStart; + @override + Duration get skipChapterEnd; /// Create a copy of NullablePlayerSettings /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/per_book_settings/models/nullable_player_settings.g.dart b/lib/features/per_book_settings/models/nullable_player_settings.g.dart index 28eb3bc..36fb655 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.g.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.g.dart @@ -32,6 +32,12 @@ _$NullablePlayerSettingsImpl _$$NullablePlayerSettingsImplFromJson( ? null : Duration( 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 _$$NullablePlayerSettingsImplToJson( @@ -44,4 +50,6 @@ Map _$$NullablePlayerSettingsImplToJson( 'speedOptions': instance.speedOptions, 'sleepTimerSettings': instance.sleepTimerSettings, 'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds, + 'skipChapterStart': instance.skipChapterStart.inMicroseconds, + 'skipChapterEnd': instance.skipChapterEnd.inMicroseconds, }; diff --git a/lib/features/per_book_settings/providers/book_settings_provider.dart b/lib/features/per_book_settings/providers/book_settings_provider.dart index ca460b6..50d8f20 100644 --- a/lib/features/per_book_settings/providers/book_settings_provider.dart +++ b/lib/features/per_book_settings/providers/book_settings_provider.dart @@ -1,8 +1,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/db/available_boxes.dart'; -import 'package:vaani/features/per_book_settings/models/book_settings.dart' - as model; +import 'package:vaani/features/per_book_settings/models/book_settings.dart' as model; import 'package:vaani/features/per_book_settings/models/nullable_player_settings.dart'; part 'book_settings_provider.g.dart'; @@ -52,6 +51,7 @@ class BookSettings extends _$BookSettings { } void update(model.BookSettings newSettings, {bool force = false}) { + state = newSettings; updateState(newSettings, force: force); } } diff --git a/lib/features/per_book_settings/providers/book_settings_provider.g.dart b/lib/features/per_book_settings/providers/book_settings_provider.g.dart index 221433f..a620d34 100644 --- a/lib/features/per_book_settings/providers/book_settings_provider.g.dart +++ b/lib/features/per_book_settings/providers/book_settings_provider.g.dart @@ -6,7 +6,7 @@ part of 'book_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$bookSettingsHash() => r'b976df954edf98ec6ccb3eb41e9d07dd4a9193eb'; +String _$bookSettingsHash() => r'ef4316367513b1b2b3971e53609e8f0f29de8667'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 87247fe..680bb4d 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -51,7 +51,18 @@ class AudiobookPlayer extends AudioPlayer { AudiobookPlayer(this.token, this.baseUrl) : super() { // set the source of the player to the first track in the book _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 BookExpanded? _book; @@ -114,9 +125,8 @@ class AudiobookPlayer extends AudioPlayer { // initialPosition ; final trackToPlay = getTrackToPlay(book, initialPosition ?? Duration.zero); final initialIndex = book.tracks.indexOf(trackToPlay); - final initialPositionInTrack = initialPosition != null - ? initialPosition - trackToPlay.startOffset - : null; + final initialPositionInTrack = + initialPosition != null ? initialPosition - trackToPlay.startOffset : null; _logger.finer('Setting audioSource'); await setAudioSource( @@ -126,8 +136,7 @@ class AudiobookPlayer extends AudioPlayer { ConcatenatingAudioSource( useLazyPreparation: true, children: book.tracks.map((track) { - final retrievedUri = - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token); + final retrievedUri = _getUri(track, downloadedUris, baseUrl: baseUrl, token: token); _logger.fine( '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: id: book.libraryItemId + track.index.toString(), // Metadata to display in the notification: - title: appSettings.notificationSettings.primaryTitle - .formatNotificationTitle(book), - album: appSettings.notificationSettings.secondaryTitle - .formatNotificationTitle(book), + title: appSettings.notificationSettings.primaryTitle.formatNotificationTitle(book), + album: appSettings.notificationSettings.secondaryTitle.formatNotificationTitle(book), artUri: artworkUri ?? Uri.parse( '$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 @override - Future seek(Duration? positionInBook, {int? index}) async { + Future seek(Duration? positionInBook, {int? index, bool b = true}) async { + if (!b) { + return super.seek(positionInBook, index: index); + } if (_book == null) { _logger.warning('No book is set, not seeking'); return; @@ -183,9 +193,43 @@ class AudiobookPlayer extends AudioPlayer { } final tracks = _book!.tracks; final trackToPlay = getTrackToPlay(_book!, positionInBook); - final index = tracks.indexOf(trackToPlay); + final i = tracks.indexOf(trackToPlay); 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 @@ -201,8 +245,7 @@ class AudiobookPlayer extends AudioPlayer { if (_book == null) { return Duration.zero; } - return bufferedPosition + - _book!.tracks[sequenceState!.currentIndex].startOffset; + return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset; } /// streams to override to suit the book instead of the current track @@ -277,8 +320,7 @@ Uri _getUri( }, ); - return uri ?? - Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); + return uri ?? Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); } extension FormatNotificationTitle on String { diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index a1a473d..eeeb190 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -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/player_form.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/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -245,6 +246,9 @@ class PlayerWhenExpanded extends HookConsumerWidget { const Spacer(), // chapter list const ChapterSelectionButton(), + const Spacer(), + // 跳过片头片尾 + SkipChapterStartEndButton(), // settings // IconButton( // icon: const Icon(Icons.more_horiz), diff --git a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart index ad47e8b..aead305 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart @@ -17,42 +17,42 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); - // add a small offset so the display does not show the previous chapter for a split second - const offset = Duration(milliseconds: 10); + // // add a small offset so the display does not show the previous chapter for a split second + // 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 - const doNotSeekBackIfLessThan = Duration(seconds: 5); + // /// 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); - /// seek forward to the next chapter - void seekForward() { - final index = player.book!.chapters.indexOf(player.currentChapter!); - if (index < player.book!.chapters.length - 1) { - player.seek( - player.book!.chapters[index + 1].start + offset, - ); - } else { - player.seek(player.currentChapter!.end); - } - } + // /// seek forward to the next chapter + // void seekForward() { + // final index = player.book!.chapters.indexOf(player.currentChapter!); + // if (index < player.book!.chapters.length - 1) { + // player.seek( + // player.book!.chapters[index + 1].start + offset, + // ); + // } else { + // player.seek(player.currentChapter!.end); + // } + // } - /// seek backward to the previous chapter or the start of the current chapter - void seekBackward() { - final currentPlayingChapterIndex = - player.book!.chapters.indexOf(player.currentChapter!); - final chapterPosition = - player.positionInBook - player.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 = player.book!.chapters[currentPlayingChapterIndex - 1]; - } else { - chapterToSeekTo = player.currentChapter!; - } - player.seek( - chapterToSeekTo.start + offset, - ); - } + // /// seek backward to the previous chapter or the start of the current chapter + // void seekBackward() { + // final currentPlayingChapterIndex = + // player.book!.chapters.indexOf(player.currentChapter!); + // final chapterPosition = + // player.positionInBook - player.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 = player.book!.chapters[currentPlayingChapterIndex - 1]; + // } else { + // chapterToSeekTo = player.currentChapter!; + // } + // player.seek( + // chapterToSeekTo.start + offset, + // ); + // } return IconButton( icon: Icon( @@ -69,9 +69,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { return; } if (isForward) { - seekForward(); + player.seekForward(); } else { - seekBackward(); + player.seekBackward(); } }, ); diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 04cbd0e..cc31e35 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart' - show audiobookPlayerProvider; +import 'package:vaani/features/player/providers/audiobook_player.dart' show audiobookPlayerProvider; import 'package:vaani/features/player/providers/currently_playing_provider.dart' show currentPlayingChapterProvider, currentlyPlayingBookProvider; -import 'package:vaani/features/player/view/player_when_expanded.dart' - show pendingPlayerModals; +import 'package:vaani/features/player/view/player_when_expanded.dart' show pendingPlayerModals; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; import 'package:vaani/main.dart' show appLogger; import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; -import 'package:vaani/shared/extensions/duration_format.dart' - show DurationFormat; +import 'package:vaani/shared/extensions/duration_format.dart' show DurationFormat; import 'package:vaani/shared/hooks.dart' show useTimer; class ChapterSelectionButton extends HookConsumerWidget { @@ -70,7 +67,7 @@ class ChapterSelectionModal extends HookConsumerWidget { ); } - useTimer(scrollToCurrentChapter, 500.ms); + useTimer(scrollToCurrentChapter, 100.ms); // useInterval(scrollToCurrentChapter, 500.ms); final theme = Theme.of(context); return Column( @@ -84,19 +81,18 @@ class ChapterSelectionModal extends HookConsumerWidget { Expanded( child: Scrollbar( child: SingleChildScrollView( + primary: true, child: currentBook?.chapters == null ? const Text('No chapters found') : Column( children: currentBook!.chapters.map( (chapter) { final isCurrent = currentChapterIndex == chapter.id; - final isPlayed = currentChapterIndex != null && - chapter.id < currentChapterIndex; + final isPlayed = + currentChapterIndex != null && chapter.id < currentChapterIndex; return ListTile( autofocus: isCurrent, - iconColor: isPlayed && !isCurrent - ? theme.disabledColor - : null, + iconColor: isPlayed && !isCurrent ? theme.disabledColor : null, title: Text( chapter.title, style: isPlayed && !isCurrent diff --git a/lib/features/skip_start_end/player_skip_chapter_start_end.dart b/lib/features/skip_start_end/player_skip_chapter_start_end.dart new file mode 100644 index 0000000..79e70a8 --- /dev/null +++ b/lib/features/skip_start_end/player_skip_chapter_start_end.dart @@ -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( + 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), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/skip_start_end/skip_start_end.dart b/lib/features/skip_start_end/skip_start_end.dart new file mode 100644 index 0000000..e6a5d9b --- /dev/null +++ b/lib/features/skip_start_end/skip_start_end.dart @@ -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 _subscriptions = []; + final throttler = Throttler(delay: Duration(seconds: 3)); + // final StreamController _playbackController = + // StreamController.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(); + } +} diff --git a/lib/features/skip_start_end/skip_start_end_provider.dart b/lib/features/skip_start_end/skip_start_end_provider.dart new file mode 100644 index 0000000..d00e361 --- /dev/null +++ b/lib/features/skip_start_end/skip_start_end_provider.dart @@ -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); + } +} diff --git a/lib/features/skip_start_end/skip_start_end_provider.g.dart b/lib/features/skip_start_end/skip_start_end_provider.g.dart new file mode 100644 index 0000000..ef202f2 --- /dev/null +++ b/lib/features/skip_start_end/skip_start_end_provider.g.dart @@ -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.internal( + SkipStartEnd.new, + name: r'skipStartEndProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SkipStartEnd = Notifier; +// 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 diff --git a/lib/features/you/view/widgets/library_switch_chip.dart b/lib/features/you/view/widgets/library_switch_chip.dart index a673332..52c23e7 100644 --- a/lib/features/you/view/widgets/library_switch_chip.dart +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' show Library; import 'package:vaani/api/library_provider.dart'; -import 'package:vaani/settings/api_settings_provider.dart' - show apiSettingsProvider; +import 'package:vaani/generated/l10n.dart'; +import 'package:vaani/settings/api_settings_provider.dart' show apiSettingsProvider; import 'package:vaani/shared/icons/abs_icons.dart'; import 'dart:io' show Platform; @@ -33,7 +33,7 @@ class LibrarySwitchChip extends HookConsumerWidget { : libraries.first.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 onPressed: libraries.isNotEmpty ? () => showLibrarySwitcher( @@ -69,7 +69,7 @@ void showLibrarySwitcher( showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const Text('Select Library'), + title: Text(S.of(context).librarySelect), content: SizedBox( // Constrain size for dialogs width: 300, // Adjust as needed @@ -83,11 +83,11 @@ void showLibrarySwitcher( ref.invalidate(librariesProvider); Navigator.pop(dialogContext); }, - child: const Text('Refresh'), + child: Text(S.of(context).refresh), ), TextButton( 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 isScrollControlled: true, constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * 0.6, // Max 60% of screen + maxHeight: MediaQuery.of(context).size.height * 0.6, // Max 60% of screen ), builder: (sheetContext) => Padding( // Add padding within the bottom sheet @@ -108,8 +107,8 @@ void showLibrarySwitcher( child: Column( mainAxisSize: MainAxisSize.min, // Take minimum necessary height children: [ - const Text( - 'Select Library', + Text( + S.of(context).librarySelect, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), @@ -121,7 +120,7 @@ void showLibrarySwitcher( const SizedBox(height: 10), ElevatedButton.icon( icon: const Icon(Icons.refresh), - label: const Text('Refresh'), + label: Text(S.of(context).refresh), onPressed: () { // Invalidate the provider to trigger a refetch ref.invalidate(librariesProvider); @@ -157,14 +156,14 @@ class _LibrarySelectionContent extends ConsumerWidget { Icon(Icons.error_outline, color: errorColor), const SizedBox(height: 10), Text( - 'Error loading libraries: $error', + S.of(context).libraryLoadError('$error'), textAlign: TextAlign.center, style: TextStyle(color: errorColor), ), const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(S.of(context).retry), onPressed: () { // Invalidate the provider to trigger a refetch ref.invalidate(librariesProvider); @@ -179,10 +178,10 @@ class _LibrarySelectionContent extends ConsumerWidget { data: (libraries) { // Handle case where data loaded successfully but is empty if (libraries.isEmpty) { - return const Center( + return Center( child: Padding( padding: EdgeInsets.all(16.0), - child: Text('No libraries available.'), + child: Text(S.of(context).libraryEmpty), ), ); } diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart index ca789db..c2e245e 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -5,6 +5,7 @@ import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/library_provider.dart' show librariesProvider; 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/generated/l10n.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/constants.dart'; import 'package:vaani/shared/utils.dart'; @@ -25,7 +26,7 @@ class YouPage extends HookConsumerWidget { // title: const Text('You'), actions: [ IconButton( - tooltip: 'Logs', + tooltip: S.of(context).logs, icon: const Icon(Icons.bug_report), onPressed: () { context.pushNamed(Routes.logs.name); @@ -38,7 +39,7 @@ class YouPage extends HookConsumerWidget { // }, // ), IconButton( - tooltip: 'Settings', + tooltip: S.of(context).settings, icon: const Icon(Icons.settings), onPressed: () { context.pushNamed(Routes.settings.name); @@ -61,14 +62,13 @@ class YouPage extends HookConsumerWidget { children: [ ActionChip( avatar: const Icon(Icons.switch_account_outlined), - label: const Text('Switch Account'), + label: Text(S.of(context).accountSwitch), onPressed: () { context.pushNamed(Routes.userManagement.name); }, ), librariesAsyncValue.when( - data: (libraries) => - LibrarySwitchChip(libraries: libraries), + data: (libraries) => LibrarySwitchChip(libraries: libraries), loading: () => const ActionChip( avatar: SizedBox( width: 18, @@ -88,13 +88,13 @@ class YouPage extends HookConsumerWidget { // Maybe show error details or allow retry ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: - Text('Failed to load libraries: $error'), + content: Text('Failed to load libraries: $error'), ), ); }, ), - ), // ActionChip( + ), + // ActionChip( // avatar: const Icon(Icons.logout), // label: const Text('Logout'), // onPressed: () { @@ -113,7 +113,7 @@ class YouPage extends HookConsumerWidget { const SizedBox(height: 16), ListTile( leading: const Icon(Icons.playlist_play), - title: const Text('My Playlists'), + title: Text(S.of(context).playlistsMine), onTap: () { // Handle navigation to playlists showNotImplementedToast(context); @@ -121,7 +121,7 @@ class YouPage extends HookConsumerWidget { ), ListTile( leading: const Icon(Icons.web), - title: const Text('Web Version'), + title: Text(S.of(context).webVersion), onTap: () { handleLaunchUrl( // get url from api and launch it @@ -131,7 +131,7 @@ class YouPage extends HookConsumerWidget { ), ListTile( leading: const Icon(Icons.help), - title: const Text('Help'), + title: Text(S.of(context).help), onTap: () { // Handle navigation to help website showNotImplementedToast(context); @@ -141,8 +141,7 @@ class YouPage extends HookConsumerWidget { icon: const Icon(Icons.info), applicationName: AppMetadata.appName, applicationVersion: AppMetadata.version, - applicationLegalese: - 'Made with ❤️ by ${AppMetadata.author}', + applicationLegalese: 'Made with ❤️ by ${AppMetadata.author}', aboutBoxChildren: [ // link to github repo ListTile( @@ -217,8 +216,7 @@ class UserBar extends HookConsumerWidget { Text( api.baseUrl.toString(), style: textTheme.bodyMedium?.copyWith( - color: - themeData.colorScheme.onSurface.withValues(alpha: 0.6), + color: themeData.colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 45d33d9..dece7b3 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -24,8 +24,12 @@ class MessageLookup extends MessageLookupByLibrary { static String m1(item) => "Deleted ${item}"; + static String m2(error) => "Error loading libraries: ${error}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "account": MessageLookupByLibrary.simpleMessage("Account"), + "accountSwitch": MessageLookupByLibrary.simpleMessage("Switch Account"), "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), "appearance": MessageLookupByLibrary.simpleMessage("Appearance"), "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( @@ -42,12 +46,20 @@ class MessageLookup extends MessageLookupByLibrary { "bookAboutDefault": MessageLookupByLibrary.simpleMessage( "Sorry, no description found", ), + "bookAuthors": MessageLookupByLibrary.simpleMessage("Authors"), + "bookDownloads": MessageLookupByLibrary.simpleMessage("Downloads"), + "bookGenres": MessageLookupByLibrary.simpleMessage("Genres"), "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( "Unabridged", ), + "bookSeries": MessageLookupByLibrary.simpleMessage("Series"), + "bookShelveEmpty": MessageLookupByLibrary.simpleMessage("Try again"), + "bookShelveEmptyText": MessageLookupByLibrary.simpleMessage( + "No shelves to display", + ), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "copyToClipboard": MessageLookupByLibrary.simpleMessage( "Copy to Clipboard", @@ -62,11 +74,30 @@ class MessageLookup extends MessageLookupByLibrary { "deleteDialog": m0, "deleted": m1, "explore": MessageLookupByLibrary.simpleMessage("explore"), + "exploreHint": MessageLookupByLibrary.simpleMessage( + "Seek and you shall discover...", + ), "exploreTooltip": MessageLookupByLibrary.simpleMessage( "Search and Explore", ), "general": MessageLookupByLibrary.simpleMessage("General"), + "help": MessageLookupByLibrary.simpleMessage("Help"), "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( "Continue Listening", ), @@ -85,10 +116,22 @@ class MessageLookup extends MessageLookupByLibrary { "Language switch", ), "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( "Browse your library", ), + "loading": MessageLookupByLibrary.simpleMessage("Loading..."), + "logs": MessageLookupByLibrary.simpleMessage("Logs"), "no": MessageLookupByLibrary.simpleMessage("No"), + "notImplemented": MessageLookupByLibrary.simpleMessage("Not implemented"), "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( "Notification Media Player", ), @@ -102,8 +145,38 @@ class MessageLookup extends MessageLookupByLibrary { "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( "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"), "readMore": MessageLookupByLibrary.simpleMessage("Read More"), + "refresh": MessageLookupByLibrary.simpleMessage("Refresh"), "reset": MessageLookupByLibrary.simpleMessage("Reset"), "resetAppSettings": MessageLookupByLibrary.simpleMessage( "Reset App Settings", @@ -132,6 +205,8 @@ class MessageLookup extends MessageLookupByLibrary { "Restore the app settings from the backup", ), "resume": MessageLookupByLibrary.simpleMessage("Resume"), + "retry": MessageLookupByLibrary.simpleMessage("Retry"), + "settings": MessageLookupByLibrary.simpleMessage("Settings"), "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( "Customize the shake detector settings", @@ -141,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "Customize the app theme", ), "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), + "webVersion": MessageLookupByLibrary.simpleMessage("Web Version"), "yes": MessageLookupByLibrary.simpleMessage("Yes"), "you": MessageLookupByLibrary.simpleMessage("You"), "youTooltip": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 5423b62..bb7cc2e 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -24,8 +24,12 @@ class MessageLookup extends MessageLookupByLibrary { static String m1(item) => "已删除 ${item}"; + static String m2(error) => "加载库时出错:${error}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "account": MessageLookupByLibrary.simpleMessage("账户"), + "accountSwitch": MessageLookupByLibrary.simpleMessage("切换账户"), "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), "appearance": MessageLookupByLibrary.simpleMessage("外观"), "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), @@ -36,19 +40,38 @@ class MessageLookup extends MessageLookupByLibrary { "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), "bookAbout": 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("取消"), "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( "将应用程序设置复制到剪贴板", ), "copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), - "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "delete": MessageLookupByLibrary.simpleMessage("删除"), "deleteDialog": m0, "deleted": m1, "explore": MessageLookupByLibrary.simpleMessage("探索"), + "exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."), "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), "general": MessageLookupByLibrary.simpleMessage("通用"), + "help": MessageLookupByLibrary.simpleMessage("Help"), "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("继续收听"), "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), @@ -59,8 +82,16 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("语言"), "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), "library": MessageLookupByLibrary.simpleMessage("媒体库"), + "libraryChange": MessageLookupByLibrary.simpleMessage("更改媒体库"), + "libraryEmpty": MessageLookupByLibrary.simpleMessage("没有可用的库。"), + "libraryLoadError": m2, + "librarySelect": MessageLookupByLibrary.simpleMessage("选择媒体库"), + "librarySwitchTooltip": MessageLookupByLibrary.simpleMessage("切换媒体库"), "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), + "loading": MessageLookupByLibrary.simpleMessage("加载中..."), + "logs": MessageLookupByLibrary.simpleMessage("日志"), "no": MessageLookupByLibrary.simpleMessage("否"), + "notImplemented": MessageLookupByLibrary.simpleMessage("未实现"), "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( "在通知中自定义媒体播放器", @@ -72,8 +103,32 @@ class MessageLookup extends MessageLookupByLibrary { "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("折叠"), "readMore": MessageLookupByLibrary.simpleMessage("展开"), + "refresh": MessageLookupByLibrary.simpleMessage("刷新"), "reset": MessageLookupByLibrary.simpleMessage("重置"), "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( @@ -90,6 +145,8 @@ class MessageLookup extends MessageLookupByLibrary { "restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), "restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), "resume": MessageLookupByLibrary.simpleMessage("继续"), + "retry": MessageLookupByLibrary.simpleMessage("重试"), + "settings": MessageLookupByLibrary.simpleMessage("设置"), "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( "自定义抖动检测器设置", @@ -97,6 +154,7 @@ class MessageLookup extends MessageLookupByLibrary { "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), "themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), + "webVersion": MessageLookupByLibrary.simpleMessage("Web版本"), "yes": MessageLookupByLibrary.simpleMessage("是"), "you": MessageLookupByLibrary.simpleMessage("我的"), "youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index f2952a1..75d6ca9 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -74,11 +74,21 @@ class S { return Intl.message('Cancel', name: 'cancel', desc: '', args: []); } + /// `Refresh` + String get refresh { + return Intl.message('Refresh', name: 'refresh', desc: '', args: []); + } + /// `Reset` String get reset { return Intl.message('Reset', name: 'reset', desc: '', args: []); } + /// `Retry` + String get retry { + return Intl.message('Retry', name: 'retry', desc: '', args: []); + } + /// `Delete` String get delete { return Intl.message('Delete', name: 'delete', desc: '', args: []); @@ -134,6 +144,16 @@ class S { 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` String get home { 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` String get bookAbout { 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` String get library { 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` String get explore { 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` String get you { 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` String get appSettings { 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` String get autoTurnOnSleepTimer { return Intl.message( @@ -568,6 +883,21 @@ class S { 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 { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f5c5ca8..9d10a8d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5,7 +5,9 @@ "no": "No", "ok": "OK", "cancel": "Cancel", + "refresh": "Refresh", "reset": "Reset", + "retry": "Retry", "delete": "Delete", "deleteDialog": "Are you sure you want to delete {item}?", "@deleteDialog": { @@ -31,31 +33,74 @@ "unknown": "Unknown", "readMore": "Read More", "readLess": "Read Less", + "loading": "Loading...", + "help": "Help", "home": "Home", "homeListenAgain": "Listen Again", "homeContinueListening": "Continue 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", "bookAboutDefault": "Sorry, no description found", "bookMetadataAbridged": "Abridged", "bookMetadataUnabridged": "Unabridged", "bookMetadataLength": "Length", "bookMetadataPublished": "Published", + "bookShelveEmpty": "Try again", + "bookShelveEmptyText": "No shelves to display", + "bookAuthors": "Authors", + "bookGenres": "Genres", + "bookSeries": "Series", + "bookDownloads": "Downloads", "library": "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", "exploreTooltip": "Search and Explore", + "exploreHint": "Seek and you shall discover...", "you": "You", "youTooltip": "Your Profile and Settings", + "settings": "Settings", + "account": "Account", + "accountSwitch": "Switch Account", + "playlistsMine": "My Playlists", + "webVersion": "Web Version", + "appSettings": "App Settings", "general": "General", "language": "Language", "languageDescription": "Language switch", "playerSettings": "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", "automaticallyDescription": "Automatically turn on the sleep timer based on the time of day", "shakeDetector": "Shake Detector", @@ -82,6 +127,11 @@ "resetAppSettings": "Reset App Settings", "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" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 6d404cb..3848819 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -5,8 +5,10 @@ "no": "否", "ok": "确定", "cancel": "取消", + "refresh": "刷新", "reset": "重置", - "delete": "Delete", + "retry": "重试", + "delete": "删除", "deleteDialog": "确定要删除 {item} 吗?", "@deleteDialog": { "placeholders": { @@ -31,30 +33,76 @@ "unknown": "未知", "readMore": "展开", "readLess": "折叠", + "loading": "加载中...", + "help": "Help", "home": "首页", "homeListenAgain": "再听一遍", "homeContinueListening": "继续收听", "homeStartListening": "开始收听", + "homeBookContinueListening": "继续收听", + "homeBookContinueSeries": "继续系列", + "homeBookRecentlyAdded": "最近添加", + "homeBookRecommended": "推荐", + "homeBookDiscover": "发现", + "homeBookListenAgain": "再听一遍", + "homeBookNewestAuthors": "最新作者", "bookAbout": "关于本书", "bookAboutDefault": "抱歉,找不到描述", + "bookMetadataAbridged": "删节版", + "bookMetadataUnabridged": "未删节版", + "bookMetadataLength": "持续时间", + "bookMetadataPublished": "发布年份", + "bookShelveEmpty": "重试", + "bookShelveEmptyText": "未查询到书架", + "bookAuthors": "作者", + "bookGenres": "风格", + "bookSeries": "系列", + "bookDownloads": "下载", "library": "媒体库", "libraryTooltip": "浏览您的媒体库", + "librarySwitchTooltip": "切换媒体库", + "libraryChange": "更改媒体库", + "librarySelect": "选择媒体库", + "libraryEmpty": "没有可用的库。", + "libraryLoadError": "加载库时出错:{error}", + "@libraryLoadError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "explore": "探索", "exploreTooltip": "搜索和探索", + "exploreHint": "搜索与探索...", "you": "我的", "youTooltip": "您的个人资料和设置", + "settings": "设置", + "account": "账户", + "accountSwitch": "切换账户", + "playlistsMine": "播放列表", + "webVersion": "Web版本", + "appSettings": "应用设置", "general": "通用", "language": "语言", "languageDescription": "语言切换", "playerSettings": "播放器设置", "playerSettingsDescription": "自定义播放器设置", + "playerSettingsRememberForEveryBook": "记住每本书的播放器设置", + "playerSettingsRememberForEveryBookDescription": "每本书都会记住播放速度、音量等设置", + "playerSettingsSpeedDefault": "默认播放速度", + "playerSettingsSpeedOptions": "播放速度选项", + "playerSettingsPlaybackReporting": "回放报告", + "playerSettingsPlaybackReportingMinimum": "回放报告最小位置", + "playerSettingsPlaybackReportingMinimumDescriptionHead": "不要报告本书前 ", + "playerSettingsPlaybackReportingMinimumDescriptionTail": " 的播放", + "playerSettingsPlaybackReportingIgnore": "忽略播放位置小于", "autoTurnOnSleepTimer": "自动开启睡眠定时器", "automaticallyDescription": "根据一天中的时间自动打开睡眠定时器", - "shakeDetector": "抖动检测器", "shakeDetectorDescription": "自定义抖动检测器设置", "appearance": "外观", @@ -79,6 +127,8 @@ "resetAppSettings": "重置应用程序设置", "resetAppSettingsDescription": "将应用程序设置重置为默认值", - "resetAppSettingsDialog": "您确定要重置应用程序设置吗?" + "resetAppSettingsDialog": "您确定要重置应用程序设置吗?", + "logs": "日志", + "notImplemented": "未实现" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 19acd7a..6affd4a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,16 +12,18 @@ import 'package:vaani/features/player/core/init.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart' show audiobookPlayerProvider, simpleAudiobookPlayerProvider; 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/generated/l10n.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_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/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; -final appLogger = Logger('vaani'); +final appLogger = Logger(AppMetadata.appName); void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -171,6 +173,7 @@ class _EagerInitialization extends ConsumerWidget { ref.watch(playbackReporterProvider); ref.watch(simpleDownloadManagerProvider); ref.watch(shakeDetectorProvider); + ref.watch(skipStartEndProvider); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); appLogger.severe(e.toString()); diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 3ba1d98..caf1456 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,11 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/app_settings_provider.dart' - show appSettingsProvider; +import 'package:vaani/settings/app_settings_provider.dart' show appSettingsProvider; +import 'package:vaani/settings/constants.dart'; import '../shared/widgets/shelves/home_shelf.dart'; @@ -21,11 +22,12 @@ class HomePage extends HookConsumerWidget { final scrollController = useScrollController(); final appSettings = ref.watch(appSettingsProvider); final homePageSettings = appSettings.homePageSettings; + return Scaffold( appBar: AppBar( title: GestureDetector( child: Text( - 'Vaani', + AppMetadata.appName, style: Theme.of(context).textTheme.headlineLarge, ), onTap: () { @@ -48,7 +50,7 @@ class HomePage extends HookConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('No shelves to display'), + Text(S.of(context).bookShelveEmptyText), // try again button ElevatedButton( onPressed: () { @@ -57,7 +59,7 @@ class HomePage extends HookConsumerWidget { ); 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 // using the id of the shelf final showPlayButton = switch (shelf.id) { - 'continue-listening' => - homePageSettings.showPlayButtonOnContinueListeningShelf, - 'continue-series' => - homePageSettings.showPlayButtonOnContinueSeriesShelf, - 'listen-again' => - homePageSettings.showPlayButtonOnListenAgainShelf, + 'continue-listening' => homePageSettings.showPlayButtonOnContinueListeningShelf, + 'continue-series' => homePageSettings.showPlayButtonOnContinueSeriesShelf, + 'listen-again' => homePageSettings.showPlayButtonOnListenAgainShelf, _ => 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( - title: shelf.label, + title: showLabel, shelf: shelf, showPlayButton: showPlayButton, ); @@ -102,8 +111,7 @@ class HomePage extends HookConsumerWidget { }, loading: () => const HomePageSkeleton(), error: (error, stack) { - if (apiSettings.activeUser == null || - apiSettings.activeServer == null) { + if (apiSettings.activeUser == null || apiSettings.activeServer == null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/settings/view/player_settings_page.dart b/lib/settings/view/player_settings_page.dart index 58b050f..2f73242 100644 --- a/lib/settings/view/player_settings_page.dart +++ b/lib/settings/view/player_settings_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/view/buttons.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; return SimpleSettingsPage( - title: const Text('Player Settings'), + title: Text(S.of(context).playerSettings), sections: [ SettingsSection( margin: const EdgeInsetsDirectional.symmetric( @@ -30,10 +31,10 @@ class PlayerSettingsPage extends HookConsumerWidget { tiles: [ // preferred settings for every book SettingsTile.switchTile( - title: const Text('Remember Player Settings for Every Book'), + title: Text(S.of(context).playerSettingsRememberForEveryBook), leading: const Icon(Icons.settings_applications), - description: const Text( - 'Settings like speed, loudness, etc. will be remembered for every book', + description: Text( + S.of(context).playerSettingsRememberForEveryBookDescription, ), initialValue: playerSettings.configurePlayerForEveryBook, onToggle: (value) { @@ -47,11 +48,10 @@ class PlayerSettingsPage extends HookConsumerWidget { // preferred default speed SettingsTile( - title: const Text('Default Speed'), + title: Text(S.of(context).playerSettingsSpeedDefault), trailing: Text( '${playerSettings.preferredDefaultSpeed}x', - style: - TextStyle(color: primaryColor, fontWeight: FontWeight.bold), + style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold), ), leading: const Icon(Icons.speed), onPressed: (context) async { @@ -72,11 +72,10 @@ class PlayerSettingsPage extends HookConsumerWidget { ), // preferred speed options SettingsTile( - title: const Text('Speed Options'), + title: Text(S.of(context).playerSettingsSpeedOptions), description: Text( playerSettings.speedOptions.map((e) => '${e}x').join(', '), - style: - TextStyle(fontWeight: FontWeight.bold, color: primaryColor), + style: TextStyle(fontWeight: FontWeight.bold, color: primaryColor), ), leading: const Icon(Icons.speed), onPressed: (context) async { @@ -100,23 +99,23 @@ class PlayerSettingsPage extends HookConsumerWidget { // Playback Reporting SettingsSection( - title: const Text('Playback Reporting'), + title: Text(S.of(context).playerSettingsPlaybackReporting), tiles: [ SettingsTile( - title: const Text('Minimum Position to Report'), + title: Text(S.of(context).playerSettingsPlaybackReportingMinimum), description: Text.rich( TextSpan( - text: 'Do not report playback for the first ', + text: S.of(context).playerSettingsPlaybackReportingMinimumDescriptionHead, children: [ TextSpan( - text: playerSettings - .minimumPositionForReporting.smartBinaryFormat, + text: playerSettings.minimumPositionForReporting.smartBinaryFormat, style: TextStyle( fontWeight: FontWeight.bold, color: primaryColor, ), ), - const TextSpan(text: ' of the book'), + TextSpan( + text: S.of(context).playerSettingsPlaybackReportingMinimumDescriptionTail), ], ), ), @@ -126,7 +125,7 @@ class PlayerSettingsPage extends HookConsumerWidget { context: context, builder: (context) { return TimeDurationSelector( - title: const Text('Ignore Playback Position Less Than'), + title: Text(S.of(context).playerSettingsPlaybackReportingIgnore), baseUnit: BaseUnit.second, initialValue: playerSettings.minimumPositionForReporting, ); @@ -149,8 +148,7 @@ class PlayerSettingsPage extends HookConsumerWidget { text: 'Mark complete when less than ', children: [ TextSpan( - text: playerSettings - .markCompleteWhenTimeLeft.smartBinaryFormat, + text: playerSettings.markCompleteWhenTimeLeft.smartBinaryFormat, style: TextStyle( fontWeight: FontWeight.bold, color: primaryColor, @@ -189,8 +187,7 @@ class PlayerSettingsPage extends HookConsumerWidget { text: 'Report progress every ', children: [ TextSpan( - text: playerSettings - .playbackReportInterval.smartBinaryFormat, + text: playerSettings.playbackReportInterval.smartBinaryFormat, style: TextStyle( fontWeight: FontWeight.bold, color: primaryColor, @@ -234,8 +231,7 @@ class PlayerSettingsPage extends HookConsumerWidget { description: const Text( 'Show the total progress of the book in the player', ), - initialValue: - playerSettings.expandedPlayerSettings.showTotalProgress, + initialValue: playerSettings.expandedPlayerSettings.showTotalProgress, onToggle: (value) { ref.read(appSettingsProvider.notifier).update( appSettings.copyWith.playerSettings @@ -250,13 +246,11 @@ class PlayerSettingsPage extends HookConsumerWidget { description: const Text( 'Show the progress of the current chapter in the player', ), - initialValue: - playerSettings.expandedPlayerSettings.showChapterProgress, + initialValue: playerSettings.expandedPlayerSettings.showChapterProgress, onToggle: (value) { ref.read(appSettingsProvider.notifier).update( appSettings.copyWith.playerSettings( - expandedPlayerSettings: playerSettings - .expandedPlayerSettings + expandedPlayerSettings: playerSettings.expandedPlayerSettings .copyWith(showChapterProgress: value), ), ); @@ -315,8 +309,7 @@ class SpeedPicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final speedController = - useTextEditingController(text: initialValue.toString()); + final speedController = useTextEditingController(text: initialValue.toString()); final speed = useState(initialValue); return AlertDialog( title: const Text('Select Speed'), @@ -375,8 +368,7 @@ class SpeedOptionsPicker extends HookConsumerWidget { onDeleted: speed == 1 ? null : () { - speedOptions.value = - speedOptions.value.where((element) { + speedOptions.value = speedOptions.value.where((element) { // speed option 1 can't be removed return element != speed; }).toList(); diff --git a/lib/shared/extensions/string.dart b/lib/shared/extensions/string.dart new file mode 100644 index 0000000..129ff60 --- /dev/null +++ b/lib/shared/extensions/string.dart @@ -0,0 +1,6 @@ +extension StringExtension on String { + String get capitalize { + if (isEmpty) return ""; + return "${this[0].toUpperCase()}${substring(1)}"; + } +} diff --git a/lib/shared/widgets/not_implemented.dart b/lib/shared/widgets/not_implemented.dart index 801f152..7d20eab 100644 --- a/lib/shared/widgets/not_implemented.dart +++ b/lib/shared/widgets/not_implemented.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:vaani/generated/l10n.dart'; void showNotImplementedToast(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Not implemented"), + SnackBar( + content: Text(S.of(context).notImplemented), showCloseIcon: true, ), );