Vaani/lib/features/player/view/widgets/chapter_selection_button.dart

136 lines
4.6 KiB
Dart
Raw Normal View History

2024-08-20 10:14:07 -04:00
import 'package:flutter/material.dart';
2025-12-26 16:12:30 +08:00
import 'package:flutter_hooks/flutter_hooks.dart';
2024-08-20 10:14:07 -04:00
import 'package:hooks_riverpod/hooks_riverpod.dart';
2025-12-26 16:12:30 +08:00
import 'package:super_sliver_list/super_sliver_list.dart';
2025-12-08 17:54:08 +08:00
import 'package:vaani/features/player/providers/abs_provider.dart';
2025-11-13 17:53:23 +08:00
import 'package:vaani/features/player/view/player_expanded.dart'
2025-10-25 10:38:56 +08:00
show pendingPlayerModals;
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
2025-11-22 15:54:29 +08:00
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
2025-10-25 10:38:56 +08:00
import 'package:vaani/shared/extensions/duration_format.dart'
show DurationFormat;
2025-12-26 16:12:30 +08:00
import 'package:vaani/shared/hooks.dart' show useLayoutEffect;
2024-08-20 10:14:07 -04:00
class ChapterSelectionButton extends HookConsumerWidget {
const ChapterSelectionButton({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Tooltip(
2025-11-22 15:54:29 +08:00
message: S.of(context).chapters,
2024-08-20 10:14:07 -04:00
child: IconButton(
icon: const Icon(Icons.menu_book_rounded),
onPressed: () async {
pendingPlayerModals++;
await showModalBottomSheet<bool>(
context: context,
2025-11-22 15:54:29 +08:00
barrierLabel: S.of(context).chapterSelect,
2024-08-20 10:14:07 -04:00
constraints: BoxConstraints(
// 40% of the screen height
2025-12-27 21:41:04 +08:00
maxHeight: MediaQuery.of(context).size.height * 0.7,
2024-08-20 10:14:07 -04:00
),
builder: (context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: ChapterSelectionModal(),
);
},
);
pendingPlayerModals--;
},
),
);
}
}
class ChapterSelectionModal extends HookConsumerWidget {
const ChapterSelectionModal({
super.key,
2025-12-29 17:56:03 +08:00
this.back = true,
2024-08-20 10:14:07 -04:00
});
2025-12-29 17:56:03 +08:00
final bool back;
2024-08-20 10:14:07 -04:00
@override
Widget build(BuildContext context, WidgetRef ref) {
2025-12-26 16:12:30 +08:00
final book = ref.watch(currentBookProvider);
2025-11-22 15:54:29 +08:00
final currentChapter = ref.watch(currentChapterProvider);
2025-12-26 16:12:30 +08:00
if (currentChapter == null || book == null) {
return SizedBox.shrink();
}
final initialIndex = book.chapters.indexOf(currentChapter);
final scrollController = useScrollController();
final listController = ListController();
2025-11-22 15:54:29 +08:00
2025-12-26 16:12:30 +08:00
// 首次布局完成之后跳转到指定位置
useLayoutEffect(() {
listController.jumpToItem(
index: initialIndex,
scrollController: scrollController,
2024-08-20 10:14:07 -04:00
alignment: 0.5,
);
2025-12-26 16:12:30 +08:00
});
final theme = Theme.of(context);
2024-08-20 10:14:07 -04:00
return Column(
children: [
ListTile(
title: Text(
2025-12-26 16:12:30 +08:00
'${S.of(context).chapters} (${initialIndex + 1}/${book.chapters.length})',
2024-08-20 10:14:07 -04:00
),
),
// scroll to current chapter after opening the dialog
Expanded(
2025-12-26 16:12:30 +08:00
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;
2025-12-30 17:02:28 +08:00
return Container(
// 自定义autofocus,防止autofocus出现在其他组件底层
decoration: isCurrent
? BoxDecoration(
color: Theme.of(context).focusColor, // 背景色
)
: null,
child: 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: () {
if (back) {
Navigator.of(context).pop();
} else {
ref.read(absPlayerProvider).switchChapter(chapter.id);
}
},
2025-12-26 16:12:30 +08:00
),
);
},
2024-08-20 10:14:07 -04:00
),
),
],
);
}
}