fix: 优化播放页面章节列表

This commit is contained in:
rang 2025-12-26 16:12:30 +08:00
parent 0a26871bb1
commit 612e8b3f50
7 changed files with 96 additions and 153 deletions

View file

@ -152,9 +152,9 @@ class PlayerState extends _$PlayerState {
} }
@riverpod @riverpod
Duration? currentTime(Ref ref, String libraryItemId) { Future<Duration?> currentTime(Ref ref, String libraryItemId) async {
final me = ref.watch(meProvider); final me = await ref.watch(meProvider.future);
final userProgress = me.valueOrNull?.mediaProgress final userProgress = me.mediaProgress
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
return userProgress?.currentTime; return userProgress?.currentTime;
} }
@ -192,7 +192,8 @@ class CurrentBook extends _$CurrentBook {
} }
final book = await ref.read(libraryItemProvider(libraryItemId).future); final book = await ref.read(libraryItemProvider(libraryItemId).future);
state = book.media.asBookExpanded; state = book.media.asBookExpanded;
final currentTime = ref.read(currentTimeProvider(libraryItemId)); final currentTime =
await ref.read(currentTimeProvider(libraryItemId).future);
await ref await ref
.read(absPlayerProvider.notifier) .read(absPlayerProvider.notifier)
.load(state!, initialPosition: currentTime, play: play); .load(state!, initialPosition: currentTime, play: play);

View file

@ -57,7 +57,7 @@ final playerActiveProvider = AutoDisposeProvider<bool>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>; typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
String _$currentTimeHash() => r'079945f118884b57d2e038117c7a7a5b873bc7d1'; String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -85,7 +85,7 @@ class _SystemHash {
const currentTimeProvider = CurrentTimeFamily(); const currentTimeProvider = CurrentTimeFamily();
/// See also [currentTime]. /// See also [currentTime].
class CurrentTimeFamily extends Family<Duration?> { class CurrentTimeFamily extends Family<AsyncValue<Duration?>> {
/// See also [currentTime]. /// See also [currentTime].
const CurrentTimeFamily(); const CurrentTimeFamily();
@ -123,7 +123,7 @@ class CurrentTimeFamily extends Family<Duration?> {
} }
/// See also [currentTime]. /// See also [currentTime].
class CurrentTimeProvider extends AutoDisposeProvider<Duration?> { class CurrentTimeProvider extends AutoDisposeFutureProvider<Duration?> {
/// See also [currentTime]. /// See also [currentTime].
CurrentTimeProvider( CurrentTimeProvider(
String libraryItemId, String libraryItemId,
@ -158,7 +158,7 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
@override @override
Override overrideWith( Override overrideWith(
Duration? Function(CurrentTimeRef provider) create, FutureOr<Duration?> Function(CurrentTimeRef provider) create,
) { ) {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
@ -175,7 +175,7 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
} }
@override @override
AutoDisposeProviderElement<Duration?> createElement() { AutoDisposeFutureProviderElement<Duration?> createElement() {
return _CurrentTimeProviderElement(this); return _CurrentTimeProviderElement(this);
} }
@ -195,13 +195,13 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin CurrentTimeRef on AutoDisposeProviderRef<Duration?> { mixin CurrentTimeRef on AutoDisposeFutureProviderRef<Duration?> {
/// The parameter `libraryItemId` of this provider. /// The parameter `libraryItemId` of this provider.
String get libraryItemId; String get libraryItemId;
} }
class _CurrentTimeProviderElement extends AutoDisposeProviderElement<Duration?> class _CurrentTimeProviderElement
with CurrentTimeRef { extends AutoDisposeFutureProviderElement<Duration?> with CurrentTimeRef {
_CurrentTimeProviderElement(super.provider); _CurrentTimeProviderElement(super.provider);
@override @override
@ -275,7 +275,7 @@ final playerStateProvider =
); );
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>; typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
String _$currentBookHash() => r'eed66894cb003d9d8ebd7b128d6ebb4efd5cda1b'; String _$currentBookHash() => r'b4f6b6ccc772631db3dfd9070be3d7487333544d';
/// See also [CurrentBook]. /// See also [CurrentBook].
@ProviderFor(CurrentBook) @ProviderFor(CurrentBook)

View file

@ -1,9 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/providers/abs_provider.dart';
import 'package:vaani/features/player/view/player_expanded.dart' import 'package:vaani/features/player/view/player_expanded.dart'
@ -11,14 +9,13 @@ import 'package:vaani/features/player/view/player_expanded.dart'
import 'package:vaani/features/player/view/player_minimized.dart'; import 'package:vaani/features/player/view/player_minimized.dart';
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart'; import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart';
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart'; import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart';
import 'package:vaani/features/player/view/widgets/chapter_selection_button.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart'; import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/features/player/view/widgets/player_progress_bar.dart'; import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart'; import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart';
import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart'; import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart';
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
import 'package:vaani/globals.dart'; import 'package:vaani/globals.dart';
import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
var pendingPlayerModals = 0; var pendingPlayerModals = 0;
@ -104,7 +101,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
), ),
), ),
), ),
child: ChapterSelection(), child: ChapterSelectionModal(),
), ),
), ),
], ],
@ -158,76 +155,3 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
); );
} }
} }
class ChapterSelection extends HookConsumerWidget {
const ChapterSelection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentChapter = ref.watch(currentChapterProvider);
if (currentChapter == null) {
return SizedBox.shrink();
}
final chapters = useState(<BookChapter>[]);
final scrollController = useScrollController();
useEffect(
() {
int page = 0;
void load(page) {
chapters.value.addAll(ref.watch(currentChaptersProvider));
}
load(page);
void listener() {
if (scrollController.position.pixels /
scrollController.position.maxScrollExtent >
0.8) {
print('滚动到底部');
}
}
scrollController.addListener(listener);
return () => scrollController.removeListener(listener);
},
[scrollController],
);
final currentChapterIndex = chapters.value.indexOf(currentChapter);
final theme = Theme.of(context);
return Scrollbar(
controller: scrollController,
child: ListView.builder(
controller: scrollController,
itemCount: chapters.value.length,
itemBuilder: (context, index) {
final chapter = chapters.value[index];
final isCurrent = currentChapterIndex == index;
final isPlayed = index < currentChapterIndex;
return ListTile(
autofocus: isCurrent,
iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
title: Text(
chapter.title,
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
),
subtitle: Text(
'(${chapter.duration.smartBinaryFormat})',
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
),
// trailing: isCurrent
// ? const PlayingIndicatorIcon()
// : const Icon(Icons.play_arrow),
selected: isCurrent,
// key: isCurrent ? chapterKey : null,
onTap: () {
ref.read(absPlayerProvider).switchChapter(chapter.id);
},
);
},
),
);
}
}

View file

@ -1,16 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/providers/abs_provider.dart';
import 'package:vaani/features/player/view/player_expanded.dart' import 'package:vaani/features/player/view/player_expanded.dart'
show pendingPlayerModals; show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
import 'package:vaani/generated/l10n.dart'; import 'package:vaani/generated/l10n.dart';
import 'package:vaani/globals.dart';
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
import 'package:vaani/shared/extensions/duration_format.dart' import 'package:vaani/shared/extensions/duration_format.dart'
show DurationFormat; show DurationFormat;
import 'package:vaani/shared/hooks.dart' show useTimer; import 'package:vaani/shared/hooks.dart' show useLayoutEffect;
class ChapterSelectionButton extends HookConsumerWidget { class ChapterSelectionButton extends HookConsumerWidget {
const ChapterSelectionButton({ const ChapterSelectionButton({
@ -53,77 +53,66 @@ class ChapterSelectionModal extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(currentBookProvider); final book = ref.watch(currentBookProvider);
final currentChapter = ref.watch(currentChapterProvider); final currentChapter = ref.watch(currentChapterProvider);
if (currentChapter == null || book == null) {
final currentChapterIndex = currentChapter?.id; return SizedBox.shrink();
final chapterKey = GlobalKey();
scrollToCurrentChapter() async {
appLogger.fine('scrolling to chapter');
await Scrollable.ensureVisible(
chapterKey.currentContext!,
duration: 200.ms,
alignment: 0.5,
curve: Curves.easeInOut,
);
} }
final initialIndex = book.chapters.indexOf(currentChapter);
final scrollController = useScrollController();
final listController = ListController();
useTimer(scrollToCurrentChapter, 100.ms); //
// useInterval(scrollToCurrentChapter, 500.ms); useLayoutEffect(() {
listController.jumpToItem(
index: initialIndex,
scrollController: scrollController,
alignment: 0.5,
);
});
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
children: [ children: [
ListTile( ListTile(
title: Text( title: Text(
'${S.of(context).chapters} ${currentChapterIndex == null ? '' : ' (${currentChapterIndex + 1}/${session?.chapters.length})'}', '${S.of(context).chapters} (${initialIndex + 1}/${book.chapters.length})',
), ),
), ),
// scroll to current chapter after opening the dialog // scroll to current chapter after opening the dialog
Expanded( Expanded(
child: Scrollbar( child: SuperListView.builder(
child: SingleChildScrollView( listController: listController,
primary: true, controller: scrollController,
child: session?.chapters == null itemCount: book.chapters.length,
? Text(S.of(context).chapterNotFound) itemBuilder: (BuildContext context, int index) {
: Column( final chapter = book.chapters[index];
children: session!.chapters.map( final isCurrent = currentChapter.id == chapter.id;
(chapter) { final isPlayed = index < initialIndex;
final isCurrent = currentChapterIndex == chapter.id; return ListTile(
final isPlayed = currentChapterIndex != null && autofocus: isCurrent,
chapter.id < currentChapterIndex; iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
return ListTile( title: Text(
autofocus: isCurrent, chapter.title,
iconColor: isPlayed && !isCurrent style: isPlayed && !isCurrent
? theme.disabledColor ? TextStyle(color: theme.disabledColor)
: null, : null,
title: Text( ),
chapter.title, subtitle: Text(
style: isPlayed && !isCurrent '(${chapter.duration.smartBinaryFormat})',
? TextStyle(color: theme.disabledColor) style: isPlayed && !isCurrent
: null, ? TextStyle(color: theme.disabledColor)
), : null,
subtitle: Text( ),
'(${chapter.duration.smartBinaryFormat})', trailing: isCurrent
style: isPlayed && !isCurrent ? const PlayingIndicatorIcon()
? TextStyle(color: theme.disabledColor) : const Icon(Icons.play_arrow),
: null, selected: isCurrent,
), onTap: () {
trailing: isCurrent Navigator.of(context).pop();
? const PlayingIndicatorIcon() ref.read(absPlayerProvider).switchChapter(chapter.id);
: const Icon(Icons.play_arrow), },
selected: isCurrent, );
key: isCurrent ? chapterKey : null, },
onTap: () {
Navigator.of(context).pop();
ref
.read(absPlayerProvider)
.switchChapter(chapter.id);
},
);
},
).toList(),
),
),
), ),
), ),
], ],

View file

@ -3,6 +3,11 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
// useEffect((){}, [])
//
// useLayoutEffect((){})
void useInterval(VoidCallback callback, Duration delay) { void useInterval(VoidCallback callback, Duration delay) {
final savedCallback = useRef(callback); final savedCallback = useRef(callback);
savedCallback.value = callback; savedCallback.value = callback;
@ -28,3 +33,18 @@ void useTimer(VoidCallback callback, Duration delay) {
[delay], [delay],
); );
} }
void useLayoutEffect(VoidCallback callback) {
final savedCallback = useRef(callback);
savedCallback.value = callback;
useEffect(
() {
WidgetsBinding.instance.addPostFrameCallback((_) {
savedCallback.value();
});
return null;
},
[],
);
}

View file

@ -1390,6 +1390,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
super_sliver_list:
dependency: "direct main"
description:
name: super_sliver_list
sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56
url: "https://pub.dev"
source: hosted
version: "0.4.1"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:

View file

@ -48,6 +48,7 @@ dependencies:
# cupertino_icons: ^1.0.6 # cupertino_icons: ^1.0.6
# flutter_platform_widgets: ^9.0.0 # flutter_platform_widgets: ^9.0.0
flutter_staggered_grid_view: ^0.7.0 flutter_staggered_grid_view: ^0.7.0
super_sliver_list: ^0.4.1
duration_picker: ^1.2.0 duration_picker: ^1.2.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
# easy_stepper: ^0.8.4 # easy_stepper: ^0.8.4