From 612e8b3f50d5ba6371d72597d8e16bc6a5b2e217 Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Fri, 26 Dec 2025 16:12:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=AB=A0=E8=8A=82=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../player/providers/abs_provider.dart | 9 +- .../player/providers/abs_provider.g.dart | 18 +-- .../player/view/player_expanded_desktop.dart | 80 +------------ .../widgets/chapter_selection_button.dart | 113 ++++++++---------- lib/shared/hooks.dart | 20 ++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 96 insertions(+), 153 deletions(-) diff --git a/lib/features/player/providers/abs_provider.dart b/lib/features/player/providers/abs_provider.dart index 9ed94c0..5fdcbfc 100644 --- a/lib/features/player/providers/abs_provider.dart +++ b/lib/features/player/providers/abs_provider.dart @@ -152,9 +152,9 @@ class PlayerState extends _$PlayerState { } @riverpod -Duration? currentTime(Ref ref, String libraryItemId) { - final me = ref.watch(meProvider); - final userProgress = me.valueOrNull?.mediaProgress +Future currentTime(Ref ref, String libraryItemId) async { + final me = await ref.watch(meProvider.future); + final userProgress = me.mediaProgress ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); return userProgress?.currentTime; } @@ -192,7 +192,8 @@ class CurrentBook extends _$CurrentBook { } final book = await ref.read(libraryItemProvider(libraryItemId).future); state = book.media.asBookExpanded; - final currentTime = ref.read(currentTimeProvider(libraryItemId)); + final currentTime = + await ref.read(currentTimeProvider(libraryItemId).future); await ref .read(absPlayerProvider.notifier) .load(state!, initialPosition: currentTime, play: play); diff --git a/lib/features/player/providers/abs_provider.g.dart b/lib/features/player/providers/abs_provider.g.dart index 3b71a3f..005e46a 100644 --- a/lib/features/player/providers/abs_provider.g.dart +++ b/lib/features/player/providers/abs_provider.g.dart @@ -57,7 +57,7 @@ final playerActiveProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PlayerActiveRef = AutoDisposeProviderRef; -String _$currentTimeHash() => r'079945f118884b57d2e038117c7a7a5b873bc7d1'; +String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9'; /// Copied from Dart SDK class _SystemHash { @@ -85,7 +85,7 @@ class _SystemHash { const currentTimeProvider = CurrentTimeFamily(); /// See also [currentTime]. -class CurrentTimeFamily extends Family { +class CurrentTimeFamily extends Family> { /// See also [currentTime]. const CurrentTimeFamily(); @@ -123,7 +123,7 @@ class CurrentTimeFamily extends Family { } /// See also [currentTime]. -class CurrentTimeProvider extends AutoDisposeProvider { +class CurrentTimeProvider extends AutoDisposeFutureProvider { /// See also [currentTime]. CurrentTimeProvider( String libraryItemId, @@ -158,7 +158,7 @@ class CurrentTimeProvider extends AutoDisposeProvider { @override Override overrideWith( - Duration? Function(CurrentTimeRef provider) create, + FutureOr Function(CurrentTimeRef provider) create, ) { return ProviderOverride( origin: this, @@ -175,7 +175,7 @@ class CurrentTimeProvider extends AutoDisposeProvider { } @override - AutoDisposeProviderElement createElement() { + AutoDisposeFutureProviderElement createElement() { return _CurrentTimeProviderElement(this); } @@ -195,13 +195,13 @@ class CurrentTimeProvider extends AutoDisposeProvider { @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin CurrentTimeRef on AutoDisposeProviderRef { +mixin CurrentTimeRef on AutoDisposeFutureProviderRef { /// The parameter `libraryItemId` of this provider. String get libraryItemId; } -class _CurrentTimeProviderElement extends AutoDisposeProviderElement - with CurrentTimeRef { +class _CurrentTimeProviderElement + extends AutoDisposeFutureProviderElement with CurrentTimeRef { _CurrentTimeProviderElement(super.provider); @override @@ -275,7 +275,7 @@ final playerStateProvider = ); typedef _$PlayerState = AutoDisposeNotifier; -String _$currentBookHash() => r'eed66894cb003d9d8ebd7b128d6ebb4efd5cda1b'; +String _$currentBookHash() => r'b4f6b6ccc772631db3dfd9070be3d7487333544d'; /// See also [CurrentBook]. @ProviderFor(CurrentBook) diff --git a/lib/features/player/view/player_expanded_desktop.dart b/lib/features/player/view/player_expanded_desktop.dart index 6c71711..7e2365f 100644 --- a/lib/features/player/view/player_expanded_desktop.dart +++ b/lib/features/player/view/player_expanded_desktop.dart @@ -1,9 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/abs_provider.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/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/chapter_selection_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_speed_adjust_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/globals.dart'; -import 'package:vaani/shared/extensions/chapter.dart'; -import 'package:vaani/shared/extensions/duration_format.dart'; 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([]); - 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); - }, - ); - }, - ), - ); - } -} diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 6ed4004..780f27f 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,16 +1,16 @@ 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:super_sliver_list/super_sliver_list.dart'; import 'package:vaani/features/player/providers/abs_provider.dart'; import 'package:vaani/features/player/view/player_expanded.dart' show pendingPlayerModals; import 'package:vaani/features/player/view/widgets/playing_indicator_icon.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/duration_format.dart' show DurationFormat; -import 'package:vaani/shared/hooks.dart' show useTimer; +import 'package:vaani/shared/hooks.dart' show useLayoutEffect; class ChapterSelectionButton extends HookConsumerWidget { const ChapterSelectionButton({ @@ -53,77 +53,66 @@ class ChapterSelectionModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(currentBookProvider); + final book = ref.watch(currentBookProvider); final currentChapter = ref.watch(currentChapterProvider); - - final currentChapterIndex = currentChapter?.id; - final chapterKey = GlobalKey(); - scrollToCurrentChapter() async { - appLogger.fine('scrolling to chapter'); - await Scrollable.ensureVisible( - chapterKey.currentContext!, - duration: 200.ms, - alignment: 0.5, - curve: Curves.easeInOut, - ); + if (currentChapter == null || book == null) { + return SizedBox.shrink(); } + 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); return Column( children: [ ListTile( 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 Expanded( - child: Scrollbar( - child: SingleChildScrollView( - primary: true, - child: session?.chapters == null - ? Text(S.of(context).chapterNotFound) - : Column( - children: session!.chapters.map( - (chapter) { - final isCurrent = currentChapterIndex == chapter.id; - final isPlayed = currentChapterIndex != null && - chapter.id < 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: () { - Navigator.of(context).pop(); - ref - .read(absPlayerProvider) - .switchChapter(chapter.id); - }, - ); - }, - ).toList(), - ), - ), + child: SuperListView.builder( + listController: listController, + controller: scrollController, + itemCount: book.chapters.length, + itemBuilder: (BuildContext context, int index) { + final chapter = book.chapters[index]; + final isCurrent = currentChapter.id == chapter.id; + final isPlayed = index < initialIndex; + 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, + onTap: () { + Navigator.of(context).pop(); + ref.read(absPlayerProvider).switchChapter(chapter.id); + }, + ); + }, ), ), ], diff --git a/lib/shared/hooks.dart b/lib/shared/hooks.dart index 8e27b3a..60998a1 100644 --- a/lib/shared/hooks.dart +++ b/lib/shared/hooks.dart @@ -3,6 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +// useEffect((){}, []) 组件构建之后执行,但并不保证布局已经完成 + +// 自定义 +// useLayoutEffect((){}) 组件布局完成之后执行 仅执行一次 + void useInterval(VoidCallback callback, Duration delay) { final savedCallback = useRef(callback); savedCallback.value = callback; @@ -28,3 +33,18 @@ void useTimer(VoidCallback callback, Duration delay) { [delay], ); } + +void useLayoutEffect(VoidCallback callback) { + final savedCallback = useRef(callback); + savedCallback.value = callback; + + useEffect( + () { + WidgetsBinding.instance.addPostFrameCallback((_) { + savedCallback.value(); + }); + return null; + }, + [], + ); +} diff --git a/pubspec.lock b/pubspec.lock index 547fdc9..f07e35f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1390,6 +1390,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b8a8818..9102064 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: # cupertino_icons: ^1.0.6 # flutter_platform_widgets: ^9.0.0 flutter_staggered_grid_view: ^0.7.0 + super_sliver_list: ^0.4.1 duration_picker: ^1.2.0 dynamic_color: ^1.7.0 # easy_stepper: ^0.8.4