2024-08-20 10:14:07 -04:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2025-04-23 16:23:57 +05:30
|
|
|
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/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/hooks.dart' show useTimer;
|
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(
|
|
|
|
|
message: 'Chapters',
|
|
|
|
|
child: IconButton(
|
|
|
|
|
icon: const Icon(Icons.menu_book_rounded),
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
pendingPlayerModals++;
|
|
|
|
|
await showModalBottomSheet<bool>(
|
|
|
|
|
context: context,
|
|
|
|
|
barrierLabel: 'Select Chapter',
|
|
|
|
|
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: ChapterSelectionModal(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
pendingPlayerModals--;
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ChapterSelectionModal extends HookConsumerWidget {
|
|
|
|
|
const ChapterSelectionModal({
|
|
|
|
|
super.key,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
|
|
|
|
final currentBook = ref.watch(currentlyPlayingBookProvider);
|
|
|
|
|
final notifier = ref.watch(audiobookPlayerProvider);
|
|
|
|
|
final currentChapterIndex = currentChapter?.id;
|
|
|
|
|
final chapterKey = GlobalKey();
|
|
|
|
|
scrollToCurrentChapter() async {
|
2024-10-03 05:54:29 -04:00
|
|
|
appLogger.fine('scrolling to chapter');
|
2024-08-20 10:14:07 -04:00
|
|
|
await Scrollable.ensureVisible(
|
|
|
|
|
chapterKey.currentContext!,
|
|
|
|
|
duration: 200.ms,
|
|
|
|
|
alignment: 0.5,
|
|
|
|
|
curve: Curves.easeInOut,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useTimer(scrollToCurrentChapter, 500.ms);
|
|
|
|
|
// useInterval(scrollToCurrentChapter, 500.ms);
|
2025-04-23 16:23:57 +05:30
|
|
|
final theme = Theme.of(context);
|
2024-08-20 10:14:07 -04:00
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text(
|
|
|
|
|
'Chapters${currentChapterIndex == null ? '' : ' (${currentChapterIndex + 1}/${currentBook?.chapters.length})'}',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// scroll to current chapter after opening the dialog
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Scrollbar(
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
child: currentBook?.chapters == null
|
|
|
|
|
? const Text('No chapters found')
|
|
|
|
|
: Column(
|
2025-04-23 16:23:57 +05:30
|
|
|
children: currentBook!.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(
|
2024-08-20 10:14:07 -04:00
|
|
|
'(${chapter.duration.smartBinaryFormat})',
|
2025-04-23 16:23:57 +05:30
|
|
|
style: isPlayed && !isCurrent
|
|
|
|
|
? TextStyle(color: theme.disabledColor)
|
|
|
|
|
: null,
|
2024-08-20 10:14:07 -04:00
|
|
|
),
|
2025-04-23 16:23:57 +05:30
|
|
|
trailing: isCurrent
|
|
|
|
|
? const PlayingIndicatorIcon()
|
|
|
|
|
: const Icon(Icons.play_arrow),
|
|
|
|
|
selected: isCurrent,
|
|
|
|
|
key: isCurrent ? chapterKey : null,
|
2024-08-20 10:14:07 -04:00
|
|
|
onTap: () {
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
notifier.seek(chapter.start + 90.ms);
|
|
|
|
|
notifier.play();
|
|
|
|
|
},
|
2025-04-23 16:23:57 +05:30
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
).toList(),
|
2024-08-20 10:14:07 -04:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|