mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-07 03:29:29 +00:00
chapter selection in player
This commit is contained in:
parent
c24541f1cd
commit
ec8304fdc3
7 changed files with 163 additions and 14 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
||||
|
|
@ -195,7 +194,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
|||
// time remaining
|
||||
Text(
|
||||
// only show 2 decimal places
|
||||
'${remainingTime.formattedBinary} left',
|
||||
'${remainingTime.smartBinaryFormat} left',
|
||||
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_prov
|
|||
show sleepTimerProvider;
|
||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||
import 'package:whispering_pages/shared/extensions/inverse_lerp.dart';
|
||||
import 'package:whispering_pages/shared/widgets/not_implemented.dart';
|
||||
|
||||
import 'widgets/audiobook_player_seek_button.dart';
|
||||
import 'widgets/audiobook_player_seek_chapter_button.dart';
|
||||
import 'widgets/chapter_selection_button.dart';
|
||||
import 'widgets/player_speed_adjust_button.dart';
|
||||
|
||||
var pendingPlayerModals = 0;
|
||||
|
|
@ -46,7 +48,6 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
earlyEnd,
|
||||
)
|
||||
.clamp(0.0, 1.0);
|
||||
final currentBook = ref.watch(currentlyPlayingBookProvider);
|
||||
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
||||
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
|
||||
|
||||
|
|
@ -86,7 +87,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
// the cast button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cast),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
showNotImplementedToast(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -240,14 +243,14 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
// sleep timer
|
||||
const SleepTimerButton(),
|
||||
// chapter list
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu_book_rounded),
|
||||
onPressed: () {},
|
||||
),
|
||||
const ChapterSelectionButton(),
|
||||
// settings
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
// show toast
|
||||
showNotImplementedToast(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
108
lib/features/player/view/widgets/chapter_selection_button.dart
Normal file
108
lib/features/player/view/widgets/chapter_selection_button.dart
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
|
||||
import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart';
|
||||
import 'package:whispering_pages/features/player/view/player_when_expanded.dart';
|
||||
import 'package:whispering_pages/shared/extensions/chapter.dart';
|
||||
import 'package:whispering_pages/shared/extensions/duration_format.dart';
|
||||
import 'package:whispering_pages/shared/hooks.dart';
|
||||
|
||||
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 {
|
||||
debugPrint('scrolling to chapter');
|
||||
await Scrollable.ensureVisible(
|
||||
chapterKey.currentContext!,
|
||||
duration: 200.ms,
|
||||
alignment: 0.5,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
useTimer(scrollToCurrentChapter, 500.ms);
|
||||
// useInterval(scrollToCurrentChapter, 500.ms);
|
||||
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(
|
||||
children: [
|
||||
for (final chapter in currentBook!.chapters)
|
||||
ListTile(
|
||||
title: Text(chapter.title),
|
||||
trailing: Text(
|
||||
'(${chapter.duration.smartBinaryFormat})',
|
||||
),
|
||||
selected: currentChapterIndex == chapter.id,
|
||||
key: currentChapterIndex == chapter.id
|
||||
? chapterKey
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
notifier.seek(chapter.start + 90.ms);
|
||||
notifier.play();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
8
lib/shared/extensions/chapter.dart
Normal file
8
lib/shared/extensions/chapter.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
|
||||
extension ChapterDuration on BookChapter {
|
||||
Duration get duration {
|
||||
// end - start
|
||||
return end - start;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
extension DurationFormat on Duration {
|
||||
/// formats the duration of the book as `10h 30m`
|
||||
/// formats the duration using only 2 units
|
||||
///
|
||||
/// will add up all the durations of the audio files first
|
||||
/// then convert them to the given format
|
||||
String get formattedBinary {
|
||||
/// if the duration is more than 1 hour, it will return `10h 30m`
|
||||
/// if the duration is less than 1 hour, it will return `30m 20s`
|
||||
/// if the duration is less than 1 minute, it will return `20s`
|
||||
String get smartBinaryFormat {
|
||||
final hours = inHours;
|
||||
final minutes = inMinutes.remainder(60);
|
||||
return '${hours}h ${minutes}m';
|
||||
final seconds = inSeconds.remainder(60);
|
||||
if (hours > 0) {
|
||||
return '${hours}h ${minutes}m';
|
||||
} else if (minutes > 0) {
|
||||
return '${minutes}m ${seconds}s';
|
||||
} else {
|
||||
return '${seconds}s';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,3 +15,16 @@ void useInterval(VoidCallback callback, Duration delay) {
|
|||
[delay],
|
||||
);
|
||||
}
|
||||
|
||||
void useTimer(VoidCallback callback, Duration delay) {
|
||||
final savedCallback = useRef(callback);
|
||||
savedCallback.value = callback;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
final timer = Timer(delay, savedCallback.value);
|
||||
return timer.cancel;
|
||||
},
|
||||
[delay],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
10
lib/shared/widgets/not_implemented.dart
Normal file
10
lib/shared/widgets/not_implemented.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void showNotImplementedToast(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Not implemented"),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue