chapter selection in player

This commit is contained in:
Dr-Blank 2024-08-20 10:14:07 -04:00
parent c24541f1cd
commit ec8304fdc3
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
7 changed files with 163 additions and 14 deletions

View file

@ -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:

View file

@ -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);
},
),
],
),

View 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();
},
),
],
),
),
),
),
],
);
}
}

View file

@ -0,0 +1,8 @@
import 'package:shelfsdk/audiobookshelf_api.dart';
extension ChapterDuration on BookChapter {
Duration get duration {
// end - start
return end - start;
}
}

View file

@ -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';
}
}
}

View file

@ -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],
);
}

View 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,
),
);
}