mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-07 11:39: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:animated_theme_switcher/animated_theme_switcher.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.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:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
||||||
|
|
@ -195,7 +194,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
// time remaining
|
// time remaining
|
||||||
Text(
|
Text(
|
||||||
// only show 2 decimal places
|
// only show 2 decimal places
|
||||||
'${remainingTime.formattedBinary} left',
|
'${remainingTime.smartBinaryFormat} left',
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color:
|
color:
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_prov
|
||||||
show sleepTimerProvider;
|
show sleepTimerProvider;
|
||||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
import 'package:whispering_pages/shared/extensions/inverse_lerp.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_button.dart';
|
||||||
import 'widgets/audiobook_player_seek_chapter_button.dart';
|
import 'widgets/audiobook_player_seek_chapter_button.dart';
|
||||||
|
import 'widgets/chapter_selection_button.dart';
|
||||||
import 'widgets/player_speed_adjust_button.dart';
|
import 'widgets/player_speed_adjust_button.dart';
|
||||||
|
|
||||||
var pendingPlayerModals = 0;
|
var pendingPlayerModals = 0;
|
||||||
|
|
@ -46,7 +48,6 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
earlyEnd,
|
earlyEnd,
|
||||||
)
|
)
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
final currentBook = ref.watch(currentlyPlayingBookProvider);
|
|
||||||
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
final currentChapter = ref.watch(currentPlayingChapterProvider);
|
||||||
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
|
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
|
||||||
|
|
||||||
|
|
@ -86,7 +87,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
// the cast button
|
// the cast button
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.cast),
|
icon: const Icon(Icons.cast),
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
showNotImplementedToast(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -240,14 +243,14 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
// sleep timer
|
// sleep timer
|
||||||
const SleepTimerButton(),
|
const SleepTimerButton(),
|
||||||
// chapter list
|
// chapter list
|
||||||
IconButton(
|
const ChapterSelectionButton(),
|
||||||
icon: const Icon(Icons.menu_book_rounded),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
// settings
|
// settings
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_horiz),
|
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 {
|
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
|
/// if the duration is more than 1 hour, it will return `10h 30m`
|
||||||
/// then convert them to the given format
|
/// if the duration is less than 1 hour, it will return `30m 20s`
|
||||||
String get formattedBinary {
|
/// if the duration is less than 1 minute, it will return `20s`
|
||||||
|
String get smartBinaryFormat {
|
||||||
final hours = inHours;
|
final hours = inHours;
|
||||||
final minutes = inMinutes.remainder(60);
|
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],
|
[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