增加跳过片头片尾,上一章下一章移动到AudioPlayer对象中

This commit is contained in:
rang 2025-10-24 11:47:50 +08:00
parent e06c834d0e
commit 620a1eb7a2
29 changed files with 1080 additions and 179 deletions

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/player/view/player_when_expanded.dart';
import 'package:vaani/settings/view/notification_settings_page.dart';
class SkipChapterStartEndButton extends HookConsumerWidget {
const SkipChapterStartEndButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Tooltip(
message: "跳过片头片尾",
child: IconButton(
icon: const Icon(Icons.fast_forward_rounded),
onPressed: () async {
// show toast
pendingPlayerModals++;
await showModalBottomSheet<bool>(
context: context,
barrierLabel: '跳过片头片尾',
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: PlayerSkipChapterStartEnd(),
);
},
);
pendingPlayerModals--;
},
),
);
}
}
class PlayerSkipChapterStartEnd extends HookConsumerWidget {
const PlayerSkipChapterStartEnd({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_';
final bookSettings = ref.watch(bookSettingsProvider(bookId));
return Scaffold(
body: Column(
children: [
ListTile(
title: Text('跳过片头 ${bookSettings.playerSettings.skipChapterStart.inSeconds}s'),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: bookSettings.playerSettings.skipChapterStart,
// defaultValue: const Duration(seconds: 0),
min: const Duration(seconds: 0),
max: const Duration(seconds: 60),
step: const Duration(seconds: 1),
onChangedEnd: (interval) {
ref
.read(
bookSettingsProvider(bookId).notifier,
)
.update(
bookSettings.copyWith.playerSettings(skipChapterStart: interval),
);
ref.read(audiobookPlayerProvider).setClip(start: interval);
},
),
),
ListTile(
title: Text('跳过片尾 ${bookSettings.playerSettings.skipChapterEnd.inSeconds}s'),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: bookSettings.playerSettings.skipChapterEnd,
// defaultValue: const Duration(seconds: 0),
min: const Duration(seconds: 0),
max: const Duration(seconds: 60),
step: const Duration(seconds: 1),
onChangedEnd: (interval) {
ref
.read(
bookSettingsProvider(bookId).notifier,
)
.update(
bookSettings.copyWith.playerSettings(skipChapterEnd: interval),
);
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,86 @@
import 'dart:async';
import 'package:vaani/features/player/core/audiobook_player.dart';
class SkipStartEnd {
final Duration start;
final Duration end;
final AudiobookPlayer player;
int _index;
final List<StreamSubscription> _subscriptions = [];
final throttler = Throttler(delay: Duration(seconds: 3));
// final StreamController<PlaybackEvent> _playbackController =
// StreamController<PlaybackEvent>.broadcast();
SkipStartEnd({required this.start, required this.end, required this.player}) : _index = 0 {
if (start > Duration()) {
_subscriptions.add(
player.currentIndexStream.listen((index) {
if (_index != index && player.position.inMilliseconds < 500) {
_index = index!;
Future.microtask(() {
player.seek(start, b: false);
});
}
}),
);
}
if (end > Duration()) {
_subscriptions.add(
player.positionStream.distinct().listen((position) {
if (player.duration != null &&
player.duration!.inMilliseconds - player.position.inMilliseconds <
end.inMilliseconds) {
Future.microtask(() {
throttler.call(player.seekForward);
});
}
}),
);
}
}
/// dispose the timer
void dispose() {
for (var sub in _subscriptions) {
sub.cancel();
}
throttler.dispose();
// _playbackController.close();
}
}
class Throttler {
final Duration delay;
Timer? _timer;
DateTime? _lastRun;
Throttler({required this.delay});
void call(void Function() callback) {
//
if (_lastRun == null) {
callback();
_lastRun = DateTime.now();
return;
}
//
if (DateTime.now().difference(_lastRun!) > delay) {
callback();
_lastRun = DateTime.now();
}
//
else {
_timer?.cancel();
_timer = Timer(delay, () {
callback();
_lastRun = DateTime.now();
});
}
}
void dispose() {
_timer?.cancel();
}
}

View file

@ -0,0 +1,23 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/features/skip_start_end/skip_start_end.dart' as core;
part 'skip_start_end_provider.g.dart';
@Riverpod(keepAlive: true)
class SkipStartEnd extends _$SkipStartEnd {
@override
core.SkipStartEnd? build() {
final player = ref.watch(audiobookPlayerProvider);
final bookId = player.book?.libraryItemId ?? '_';
if (bookId == '_') {
return null;
}
final bookSettings = ref.watch(bookSettingsProvider(bookId));
final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd;
return core.SkipStartEnd(start: start, end: end, player: player);
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'skip_start_end_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$skipStartEndHash() => r'202cfb36fdb3d3fa12debfb188f87650473a88a9';
/// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd)
final skipStartEndProvider =
NotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
SkipStartEnd.new,
name: r'skipStartEndProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SkipStartEnd = Notifier<core.SkipStartEnd?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package