mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 14:29:35 +00:00
增加跳过片头片尾,上一章下一章移动到AudioPlayer对象中
This commit is contained in:
parent
e06c834d0e
commit
620a1eb7a2
29 changed files with 1080 additions and 179 deletions
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib/features/skip_start_end/skip_start_end.dart
Normal file
86
lib/features/skip_start_end/skip_start_end.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
23
lib/features/skip_start_end/skip_start_end_provider.dart
Normal file
23
lib/features/skip_start_end/skip_start_end_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
25
lib/features/skip_start_end/skip_start_end_provider.g.dart
Normal file
25
lib/features/skip_start_end/skip_start_end_provider.g.dart
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue