mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
+
This commit is contained in:
parent
bccd778d4e
commit
2e1c29ae8a
5 changed files with 359 additions and 187 deletions
|
|
@ -8,6 +8,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
|
|||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/video.dart' as vid;
|
||||
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/indicator_builder.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
|
|
@ -18,7 +20,6 @@ import 'package:mangayomi/utils/media_query.dart';
|
|||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart';
|
||||
|
||||
class AnimePlayerView extends riv.ConsumerStatefulWidget {
|
||||
final Chapter episode;
|
||||
|
|
@ -177,13 +178,15 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
|
||||
bool _seekToCurrentPosition = true;
|
||||
bool _initSubtitle = true;
|
||||
late Duration _currentPosition = _streamController.geTCurrentPosition();
|
||||
Duration? _currentTotalDuration;
|
||||
final _showFitLabel = StateProvider((ref) => false);
|
||||
final _showSeekTo = StateProvider((ref) => false);
|
||||
late final ValueNotifier<Duration> _currentPosition =
|
||||
ValueNotifier(_streamController.geTCurrentPosition());
|
||||
final ValueNotifier<Duration?> _currentTotalDuration = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _showFitLabel = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _showSeekTo = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isCompleted = ValueNotifier(false);
|
||||
final _fit = StateProvider((ref) => BoxFit.contain);
|
||||
final _seekTo = StateProvider((ref) => 0);
|
||||
final ValueNotifier<Duration?> _tempPosition = ValueNotifier(null);
|
||||
final ValueNotifier<BoxFit> _fit = ValueNotifier(BoxFit.contain);
|
||||
final ValueNotifier<int> _seekTo = ValueNotifier(0);
|
||||
|
||||
final bool _isDesktop =
|
||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
|
@ -191,14 +194,15 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
late StreamSubscription<Duration> _currentPositionSub =
|
||||
_player.stream.position.listen(
|
||||
(position) async {
|
||||
if (_seekToCurrentPosition && _currentPosition != Duration.zero) {
|
||||
if (_seekToCurrentPosition && _currentPosition.value != Duration.zero) {
|
||||
await _player.stream.buffer.first;
|
||||
_player.seek(_currentPosition);
|
||||
_isCompleted.value =
|
||||
_player.state.duration.inSeconds - _currentPosition.inSeconds <= 10;
|
||||
_player.seek(_currentPosition.value);
|
||||
_isCompleted.value = _player.state.duration.inSeconds -
|
||||
_currentPosition.value.inSeconds <=
|
||||
10;
|
||||
_seekToCurrentPosition = false;
|
||||
} else {
|
||||
_currentPosition = position;
|
||||
_currentPosition.value = position;
|
||||
}
|
||||
if ((_firstVid.subtitles ?? []).isNotEmpty) {
|
||||
if (_initSubtitle) {
|
||||
|
|
@ -213,8 +217,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
|
||||
late final StreamSubscription<Duration> _currentTotalDurationSub =
|
||||
_player.stream.duration.listen(
|
||||
(position) {
|
||||
_currentTotalDuration = position;
|
||||
(duration) {
|
||||
_currentTotalDuration.value = duration;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -240,7 +244,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
|
||||
void _setCurrentPosition(bool save) {
|
||||
_streamController.setCurrentPosition(
|
||||
_currentPosition, _currentTotalDuration,
|
||||
_currentPosition.value, _currentTotalDuration.value,
|
||||
save: save);
|
||||
_streamController.setAnimeHistoryUpdate();
|
||||
}
|
||||
|
|
@ -310,16 +314,16 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
_currentPositionSub = _player.stream.position.listen(
|
||||
(position) async {
|
||||
if (_seekToCurrentPosition &&
|
||||
_currentPosition != Duration.zero) {
|
||||
_currentPosition.value != Duration.zero) {
|
||||
await _player.stream.buffer.first;
|
||||
_player.seek(_currentPosition);
|
||||
_player.seek(_currentPosition.value);
|
||||
try {
|
||||
_player.setSubtitleTrack(_subtitle.value!);
|
||||
} catch (_) {}
|
||||
|
||||
_seekToCurrentPosition = false;
|
||||
} else {
|
||||
_currentPosition = position;
|
||||
_currentPosition.value = position;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -655,35 +659,39 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
BoxFit.fitWidth,
|
||||
BoxFit.none
|
||||
];
|
||||
ref.read(_showFitLabel.notifier).state = true;
|
||||
_showFitLabel.value = true;
|
||||
BoxFit? fit;
|
||||
if (fitList.indexOf(ref.watch(_fit)) < fitList.length - 1) {
|
||||
fit = fitList[fitList.indexOf(ref.watch(_fit)) + 1];
|
||||
if (fitList.indexOf(_fit.value) < fitList.length - 1) {
|
||||
fit = fitList[fitList.indexOf(_fit.value) + 1];
|
||||
} else {
|
||||
fit = fitList[0];
|
||||
}
|
||||
ref.read(_fit.notifier).state = fit;
|
||||
_fit.value = fit;
|
||||
_key.currentState?.update(fit: fit);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
ref.read(_showFitLabel.notifier).state = false;
|
||||
_showFitLabel.value = false;
|
||||
}
|
||||
|
||||
Widget _seekToWidget() {
|
||||
final defaultSkipIntroLength =
|
||||
ref.watch(defaultSkipIntroLengthStateProvider);
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
height: 35,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
ref.read(_seekTo.notifier).state = defaultSkipIntroLength;
|
||||
ref.read(_showSeekTo.notifier).state = true;
|
||||
_seekTo.value = defaultSkipIntroLength;
|
||||
_showSeekTo.value = true;
|
||||
await _player.seek(Duration(
|
||||
seconds: _currentPosition.inSeconds + defaultSkipIntroLength));
|
||||
ref.read(_seekTo.notifier).state = 0;
|
||||
ref.read(_showSeekTo.notifier).state = false;
|
||||
seconds:
|
||||
_currentPosition.value.inSeconds + defaultSkipIntroLength));
|
||||
_seekTo.value = 0;
|
||||
_showSeekTo.value = false;
|
||||
},
|
||||
child: Text("+$defaultSkipIntroLength",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text("+$defaultSkipIntroLength",
|
||||
style: const TextStyle(fontWeight: FontWeight.w100)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -736,26 +744,16 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: MaterialMobilePositionIndicator(left: true))),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 7),
|
||||
child: MaterialSeekBar())),
|
||||
),
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: MaterialMobilePositionIndicator(left: false)))
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: CustomSeekBar(
|
||||
player: _controller.player,
|
||||
onSeekStart: (start) {
|
||||
_tempPosition.value = start;
|
||||
},
|
||||
onSeekEnd: (end) {
|
||||
_tempPosition.value = null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -783,7 +781,17 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20, child: MaterialDesktopSeekBar()),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: CustomSeekBar(
|
||||
player: _controller.player,
|
||||
onSeekStart: (start) {
|
||||
_tempPosition.value = start;
|
||||
},
|
||||
onSeekEnd: (end) {
|
||||
_tempPosition.value = null;
|
||||
},
|
||||
)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -931,9 +939,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
}
|
||||
|
||||
Widget _videoPlayer(BuildContext context) {
|
||||
final fit = ref.watch(_fit);
|
||||
final fit = _fit.value;
|
||||
_resize(fit);
|
||||
final seekTo = ref.watch(_seekTo);
|
||||
return Stack(
|
||||
children: [
|
||||
Video(
|
||||
|
|
@ -953,38 +960,42 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
height: mediaHeight(context, 1),
|
||||
resumeUponEnteringForegroundMode: true,
|
||||
),
|
||||
if (ref.watch(_showSeekTo))
|
||||
Positioned.fill(
|
||||
child: UnconstrainedBox(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(64.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 17, vertical: 8),
|
||||
child: Text(
|
||||
"+ $seekTo",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(_showFitLabel))
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
fit.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20),
|
||||
)))
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showSeekTo,
|
||||
builder: (context, showSeekTo, child) => showSeekTo
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: _seekTo,
|
||||
builder: (context, seekTo, child) => Positioned.fill(
|
||||
child: UnconstrainedBox(
|
||||
child: Text(
|
||||
"[+${Duration(seconds: seekTo).label()}]",
|
||||
style: const TextStyle(
|
||||
fontSize: 40.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container()),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showFitLabel,
|
||||
builder: (context, showFitLabel, child) => showFitLabel
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: _fit,
|
||||
builder: (context, fit, child) => Positioned.fill(
|
||||
child: Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
fit.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 40.0),
|
||||
))),
|
||||
),
|
||||
)
|
||||
: Container()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -998,37 +1009,36 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
seekOnDoubleTap: true,
|
||||
seekGesture: true,
|
||||
horizontalGestureSensitivity: 5000,
|
||||
verticalGestureSensitivity: 300,
|
||||
controlsHoverDuration: const Duration(seconds: 10),
|
||||
verticalGestureSensitivity: 1000,
|
||||
controlsHoverDuration: const Duration(seconds: 10000),
|
||||
volumeGesture: true,
|
||||
brightnessGesture: true,
|
||||
seekBarThumbSize: 15,
|
||||
seekBarHeight: 5,
|
||||
displaySeekBar: false,
|
||||
volumeIndicatorBuilder: (_, value) =>
|
||||
MediaIndicatorBuilder(value: value, isVolumeIndicator: true),
|
||||
brightnessIndicatorBuilder: (_, value) =>
|
||||
MediaIndicatorBuilder(value: value, isVolumeIndicator: false),
|
||||
seekIndicatorBuilder: (context, duration) {
|
||||
final swipeDuration = duration.inSeconds;
|
||||
final value = _currentPosition.inSeconds + swipeDuration;
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(64.0),
|
||||
),
|
||||
height: 52.0,
|
||||
width: 108.0,
|
||||
child: Text(
|
||||
Duration(seconds: value).label(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
);
|
||||
return _seekIndicatorTextWidget(duration, _currentPosition.value);
|
||||
},
|
||||
seekBarPositionColor: primaryColor(context),
|
||||
seekBarThumbColor: primaryColor(context),
|
||||
primaryButtonBar: _mobilePrimaryButtonBar(isFullScreen),
|
||||
primaryButtonBar: [
|
||||
ValueListenableBuilder<Duration?>(
|
||||
valueListenable: _tempPosition,
|
||||
builder: (context, snapshot, _) {
|
||||
return snapshot != null
|
||||
? _seekIndicatorTextWidget(
|
||||
snapshot, _currentPosition.value)
|
||||
: Expanded(
|
||||
child: Row(
|
||||
children: _mobilePrimaryButtonBar(isFullScreen),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
topButtonBarMargin: const EdgeInsets.all(0),
|
||||
topButtonBar: _topButtonBar(context, isFullScreen),
|
||||
bottomButtonBarMargin: const EdgeInsets.only(left: 8, right: 8),
|
||||
|
|
@ -1057,14 +1067,14 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
chapter: _streamController.getPrevEpisode());
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.skip_previous,
|
||||
size: 30,
|
||||
color: Colors.white,
|
||||
size: 35,
|
||||
color: hasPrevEpisode ? Colors.white : Colors.grey,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const MaterialPlayOrPauseButton(iconSize: 55),
|
||||
const MaterialPlayOrPauseButton(iconSize: 65),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: hasNextEpisode
|
||||
|
|
@ -1078,7 +1088,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.skip_next, size: 30, color: Colors.white),
|
||||
icon: Icon(Icons.skip_next,
|
||||
size: 35, color: hasPrevEpisode ? Colors.white : Colors.grey),
|
||||
),
|
||||
const Spacer(flex: 3)
|
||||
];
|
||||
|
|
@ -1095,7 +1106,17 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
topButtonBarMargin: const EdgeInsets.all(0),
|
||||
bottomButtonBarMargin: const EdgeInsets.all(0),
|
||||
topButtonBar: _topButtonBar(context, isFullScreen),
|
||||
buttonBarHeight: 100,
|
||||
primaryButtonBar: [
|
||||
ValueListenableBuilder<Duration?>(
|
||||
valueListenable: _tempPosition,
|
||||
builder: (context, snapshot, _) {
|
||||
return snapshot != null
|
||||
? _seekIndicatorTextWidget(
|
||||
snapshot, _currentPosition.value)
|
||||
: const SizedBox.shrink();
|
||||
})
|
||||
],
|
||||
buttonBarHeight: 110,
|
||||
displaySeekBar: false,
|
||||
seekBarThumbSize: 15,
|
||||
bottomButtonBar: _desktopBottomButtonBar(context, isFullScreen));
|
||||
|
|
@ -1121,77 +1142,24 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
|
|||
}
|
||||
}
|
||||
|
||||
class MaterialMobilePositionIndicator extends StatefulWidget {
|
||||
final bool left;
|
||||
|
||||
/// Overriden [TextStyle] for the [MaterialMobilePositionIndicator].
|
||||
final TextStyle? style;
|
||||
const MaterialMobilePositionIndicator(
|
||||
{super.key, this.style, required this.left});
|
||||
|
||||
@override
|
||||
MaterialMobilePositionIndicatorState createState() =>
|
||||
MaterialMobilePositionIndicatorState();
|
||||
}
|
||||
|
||||
class MaterialMobilePositionIndicatorState
|
||||
extends State<MaterialMobilePositionIndicator> {
|
||||
late Duration position = controller(context).player.state.position;
|
||||
late Duration duration = controller(context).player.state.duration;
|
||||
|
||||
final List<StreamSubscription> subscriptions = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (subscriptions.isEmpty) {
|
||||
subscriptions.addAll(
|
||||
[
|
||||
controller(context).player.stream.position.listen((event) {
|
||||
setState(() {
|
||||
position = event;
|
||||
});
|
||||
}),
|
||||
controller(context).player.stream.duration.listen((event) {
|
||||
setState(() {
|
||||
duration = event;
|
||||
});
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.left
|
||||
? Text(
|
||||
position.label(reference: duration),
|
||||
style: widget.style ??
|
||||
const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
duration.label(reference: duration),
|
||||
style: widget.style ??
|
||||
const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _seekIndicatorTextWidget(Duration duration, Duration currentPosition) {
|
||||
final swipeDuration = duration.inSeconds;
|
||||
final value = currentPosition.inSeconds + swipeDuration;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
Duration(seconds: value).label(),
|
||||
style: const TextStyle(
|
||||
fontSize: 65.0, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
"[${swipeDuration > 0 ? "+${Duration(seconds: swipeDuration).label()}" : "-${Duration(seconds: swipeDuration).label()}"}]",
|
||||
style: const TextStyle(
|
||||
fontSize: 40.0, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class VideoPrefs {
|
||||
|
|
|
|||
|
|
@ -150,7 +150,9 @@ class AnimeStreamController extends _$AnimeStreamController {
|
|||
if (episode.isRead!) return;
|
||||
if (incognitoMode) return;
|
||||
final markEpisodeAsSeenType = ref.watch(markEpisodeAsSeenTypeStateProvider);
|
||||
final isWatch = totalDuration != null
|
||||
final isWatch = totalDuration != null &&
|
||||
totalDuration != Duration.zero &&
|
||||
duration != Duration.zero
|
||||
? duration.inSeconds >=
|
||||
((totalDuration.inSeconds * markEpisodeAsSeenType) / 100).ceil()
|
||||
: false;
|
||||
|
|
|
|||
130
lib/modules/anime/widgets/custom_seekbar.dart
Normal file
130
lib/modules/anime/widgets/custom_seekbar.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
|
||||
class CustomSeekBar extends StatefulWidget {
|
||||
final Player player;
|
||||
final Function(Duration) onSeekStart;
|
||||
final Function(Duration) onSeekEnd;
|
||||
|
||||
const CustomSeekBar(
|
||||
{super.key,
|
||||
required this.onSeekStart,
|
||||
required this.onSeekEnd,
|
||||
required this.player});
|
||||
|
||||
@override
|
||||
CustomSeekBarState createState() => CustomSeekBarState();
|
||||
}
|
||||
|
||||
class CustomSeekBarState extends State<CustomSeekBar> {
|
||||
Duration? tempPosition;
|
||||
late Player player = widget.player;
|
||||
Duration position = Duration.zero;
|
||||
late Duration duration = player.state.duration;
|
||||
Duration buffer = Duration.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
player.stream.position.listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
position = event;
|
||||
});
|
||||
}
|
||||
});
|
||||
player.stream.duration.listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
duration = event;
|
||||
});
|
||||
}
|
||||
});
|
||||
player.stream.buffer.listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
buffer = event;
|
||||
});
|
||||
}
|
||||
});
|
||||
position = player.state.position;
|
||||
duration = player.state.duration;
|
||||
buffer = player.state.buffer;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: Text(
|
||||
tempPosition != null
|
||||
? tempPosition!.label(reference: duration)
|
||||
: position.label(reference: duration),
|
||||
style: const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
))),
|
||||
Expanded(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: isDesktop ? null : 3,
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
max: max(duration.inMilliseconds.toDouble(), 0),
|
||||
value: max(
|
||||
(tempPosition ?? position).inMilliseconds.toDouble(), 0),
|
||||
secondaryTrackValue: max(buffer.inMilliseconds.toDouble(), 0),
|
||||
onChanged: (value) {
|
||||
widget.onSeekStart(Duration(
|
||||
milliseconds: value.toInt() - position.inMilliseconds));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
tempPosition = Duration(milliseconds: value.toInt());
|
||||
});
|
||||
}
|
||||
},
|
||||
onChangeEnd: (value) async {
|
||||
widget.onSeekEnd(Duration(
|
||||
milliseconds: value.toInt() - position.inMilliseconds));
|
||||
widget.player.seek(Duration(milliseconds: value.toInt()));
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
tempPosition = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: Text(
|
||||
duration.label(reference: duration),
|
||||
style: const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/modules/anime/widgets/indicator_builder.dart
Normal file
72
lib/modules/anime/widgets/indicator_builder.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaIndicatorBuilder extends StatelessWidget {
|
||||
final bool isVolumeIndicator;
|
||||
final double value;
|
||||
const MediaIndicatorBuilder(
|
||||
{super.key, required this.value, required this.isVolumeIndicator});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
isVolumeIndicator ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
width: 30,
|
||||
child: UnconstrainedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Text((value * 100).ceil().toString()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white54,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(170, 10),
|
||||
child: LinearProgressIndicator(
|
||||
value: value,
|
||||
backgroundColor: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isVolumeIndicator
|
||||
? switch (value) {
|
||||
== 0.0 => Icons.volume_off,
|
||||
< 0.5 => Icons.volume_down,
|
||||
_ => Icons.volume_up,
|
||||
}
|
||||
: switch (value) {
|
||||
< 1.0 / 3.0 => Icons.brightness_low,
|
||||
< 2.0 / 3.0 => Icons.brightness_medium,
|
||||
_ => Icons.brightness_high,
|
||||
},
|
||||
color: Colors.white,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ class SourcesScreen extends ConsumerWidget {
|
|||
List<Source> sources = snapshot.data!
|
||||
.where((element) => showNSFW ? true : element.isNsfw == false)
|
||||
.toList();
|
||||
if (sources.isEmpty) {
|
||||
if (sources.isEmpty && !useTestSourceCode) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue