mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 14:29:35 +00:00
增加跳过片头片尾,上一章下一章移动到AudioPlayer对象中
This commit is contained in:
parent
e06c834d0e
commit
620a1eb7a2
29 changed files with 1080 additions and 179 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <Widget>[const ListTile(title: Text('Loading...'))];
|
||||
return <Widget>[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,
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ Future<void> 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<void> libraryItemPlayButtonOnPressed({
|
|||
? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed
|
||||
: appPlayerSettings.preferredDefaultSpeed,
|
||||
),
|
||||
// player.setClip(start: Duration(seconds: 10)),
|
||||
]);
|
||||
|
||||
// toggle play/pause
|
||||
|
|
|
|||
|
|
@ -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: () {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ class NullablePlayerSettings with _$NullablePlayerSettings {
|
|||
List<double>? speedOptions,
|
||||
SleepTimerSettings? sleepTimerSettings,
|
||||
Duration? playbackReportInterval,
|
||||
@Default(Duration()) Duration skipChapterStart,
|
||||
@Default(Duration()) Duration skipChapterEnd,
|
||||
}) = _NullablePlayerSettings;
|
||||
|
||||
factory NullablePlayerSettings.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
|
@ -55,7 +57,9 @@ abstract class $NullablePlayerSettingsCopyWith<$Res> {
|
|||
double? preferredDefaultSpeed,
|
||||
List<double>? 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<double>? 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<double>? speedOptions,
|
||||
this.sleepTimerSettings,
|
||||
this.playbackReportInterval})
|
||||
this.playbackReportInterval,
|
||||
this.skipChapterStart = const Duration(),
|
||||
this.skipChapterEnd = const Duration()})
|
||||
: _speedOptions = speedOptions;
|
||||
|
||||
factory _$NullablePlayerSettingsImpl.fromJson(Map<String, dynamic> 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<double>? speedOptions,
|
||||
final SleepTimerSettings? sleepTimerSettings,
|
||||
final Duration? playbackReportInterval}) = _$NullablePlayerSettingsImpl;
|
||||
final Duration? playbackReportInterval,
|
||||
final Duration skipChapterStart,
|
||||
final Duration skipChapterEnd}) = _$NullablePlayerSettingsImpl;
|
||||
|
||||
factory _NullablePlayerSettings.fromJson(Map<String, dynamic> 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.
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> _$$NullablePlayerSettingsImplToJson(
|
||||
|
|
@ -44,4 +50,6 @@ Map<String, dynamic> _$$NullablePlayerSettingsImplToJson(
|
|||
'speedOptions': instance.speedOptions,
|
||||
'sleepTimerSettings': instance.sleepTimerSettings,
|
||||
'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds,
|
||||
'skipChapterStart': instance.skipChapterStart.inMicroseconds,
|
||||
'skipChapterEnd': instance.skipChapterEnd.inMicroseconds,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
_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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib/features/skip_start_end/skip_start_end.dart
Normal file
86
lib/features/skip_start_end/skip_start_end.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
23
lib/features/skip_start_end/skip_start_end_provider.dart
Normal file
23
lib/features/skip_start_end/skip_start_end_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
25
lib/features/skip_start_end/skip_start_end_provider.g.dart
Normal file
25
lib/features/skip_start_end/skip_start_end_provider.g.dart
Normal 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
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue