mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-20 19:12:04 +00:00
Merge pull request #457 from NBA2K1/main
Improve Video Playback UI & Maintainability
This commit is contained in:
commit
fc5776ab60
6 changed files with 148 additions and 245 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -246,7 +246,7 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install webkit2gtk-4.1 clang cmake ninja-build pkg-config libgtk-3-dev mpv libmpv-dev dpkg-dev libblkid-dev liblzma-dev fuse rpm
|
||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage"
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x appimagetool
|
||||
sudo mv appimagetool /usr/local/bin/
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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/aniskip_countdown_btn.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/play_or_pause_button.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/mobile.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart';
|
||||
|
|
@ -765,18 +766,6 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
_playbackSpeed.value = speed;
|
||||
}
|
||||
|
||||
void _togglePlaybackSpeed() {
|
||||
List<double> allowedSpeeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
|
||||
if (allowedSpeeds.indexOf(_playbackSpeed.value) <
|
||||
allowedSpeeds.length - 1) {
|
||||
_setPlaybackSpeed(
|
||||
allowedSpeeds[allowedSpeeds.indexOf(_playbackSpeed.value) + 1],
|
||||
);
|
||||
} else {
|
||||
_setPlaybackSpeed(allowedSpeeds[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeFitLabel(WidgetRef ref) async {
|
||||
List<BoxFit> fitList = [
|
||||
BoxFit.contain,
|
||||
|
|
@ -848,60 +837,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_seekToWidget(),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(5),
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(
|
||||
Icons.video_settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _playbackSpeed,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
"${value}x",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_togglePlaybackSpeed();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.fit_screen_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
_changeFitLabel(ref);
|
||||
},
|
||||
),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _enterFullScreen,
|
||||
builder: (context, snapshot, _) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
_setLandscapeMode(!snapshot);
|
||||
_enterFullScreen.value = !snapshot;
|
||||
},
|
||||
icon: Icon(
|
||||
snapshot ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||
),
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
children: [_seekToWidget(), _buildSettingsButtons(context)],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -935,8 +871,9 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
},
|
||||
icon: const Icon(Icons.skip_previous, color: Colors.white),
|
||||
),
|
||||
CustomeMaterialDesktopPlayOrPauseButton(
|
||||
CustomPlayOrPauseButton(
|
||||
controller: _controller,
|
||||
isDesktop: _isDesktop,
|
||||
),
|
||||
if (hasNextEpisode)
|
||||
IconButton(
|
||||
|
|
@ -1048,44 +985,66 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
|||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(Icons.video_settings, color: Colors.white),
|
||||
),
|
||||
TextButton(
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _playbackSpeed,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
"${value}x",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_togglePlaybackSpeed();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.fit_screen_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
_changeFitLabel(ref);
|
||||
},
|
||||
),
|
||||
CustomMaterialDesktopFullscreenButton(controller: _controller),
|
||||
],
|
||||
),
|
||||
_buildSettingsButtons(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// helper method for _mobileBottomButtonBar() and _desktopBottomButtonBar()
|
||||
Widget _buildSettingsButtons(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: _isDesktop ? EdgeInsets.zero : const EdgeInsets.all(5),
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(Icons.video_settings, color: Colors.white),
|
||||
),
|
||||
PopupMenuButton<double>(
|
||||
tooltip: '', // Remove default tooltip "Show menu" for consistency
|
||||
icon: const Icon(Icons.speed, color: Colors.white),
|
||||
itemBuilder:
|
||||
(context) =>
|
||||
[0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0]
|
||||
.map(
|
||||
(speed) => PopupMenuItem<double>(
|
||||
value: speed,
|
||||
child: Text("${speed}x"),
|
||||
onTap: () {
|
||||
_setPlaybackSpeed(speed);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fit_screen_outlined, color: Colors.white),
|
||||
onPressed: () async {
|
||||
_changeFitLabel(ref);
|
||||
},
|
||||
),
|
||||
if (_isDesktop)
|
||||
CustomMaterialDesktopFullscreenButton(controller: _controller)
|
||||
else
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _enterFullScreen,
|
||||
builder: (context, snapshot, _) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
_setLandscapeMode(!snapshot);
|
||||
_enterFullScreen.value = !snapshot;
|
||||
},
|
||||
icon: Icon(snapshot ? Icons.fullscreen_exit : Icons.fullscreen),
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _topButtonBar(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _enterFullScreen,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ class CustomSeekBarState extends State<CustomSeekBar> {
|
|||
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxValue = max(duration.inMilliseconds.toDouble(), 0).toDouble();
|
||||
final rawValue =
|
||||
(widget.delta ?? tempPosition ?? position).inMilliseconds.toDouble();
|
||||
final clampedValue = rawValue.clamp(0, maxValue).toDouble();
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
|
|
@ -88,12 +92,8 @@ class CustomSeekBarState extends State<CustomSeekBar> {
|
|||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
max: max(duration.inMilliseconds.toDouble(), 0),
|
||||
value: max(
|
||||
(widget.delta ?? tempPosition ?? position).inMilliseconds
|
||||
.toDouble(),
|
||||
0,
|
||||
),
|
||||
max: maxValue,
|
||||
value: clampedValue,
|
||||
secondaryTrackValue: max(buffer.inMilliseconds.toDouble(), 0),
|
||||
onChanged: (value) {
|
||||
widget.onSeekStart?.call(
|
||||
|
|
|
|||
|
|
@ -500,75 +500,6 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
|
|||
}
|
||||
}
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
||||
/// A material design play/pause button.
|
||||
class CustomeMaterialDesktopPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoController controller;
|
||||
|
||||
const CustomeMaterialDesktopPlayOrPauseButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
CustomeMaterialDesktopPlayOrPauseButtonState createState() =>
|
||||
CustomeMaterialDesktopPlayOrPauseButtonState();
|
||||
}
|
||||
|
||||
class CustomeMaterialDesktopPlayOrPauseButtonState
|
||||
extends State<CustomeMaterialDesktopPlayOrPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animation = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.controller.player.state.playing ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
StreamSubscription<bool>? subscription;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
subscription ??= widget.controller.player.stream.playing.listen((event) {
|
||||
if (event) {
|
||||
animation.forward();
|
||||
} else {
|
||||
animation.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animation.dispose();
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: widget.controller.player.playOrPause,
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
icon: AnimatedIcon(
|
||||
progress: animation,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BUTTON: VOLUME
|
||||
|
||||
/// MaterialDesktop design volume button & slider.
|
||||
|
|
@ -804,8 +735,12 @@ class CustomMaterialDesktopPositionIndicatorState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clampedPosition = (widget.delta ?? position).clamp(
|
||||
Duration.zero,
|
||||
duration,
|
||||
);
|
||||
return Text(
|
||||
'${(widget.delta ?? position).label(reference: duration)} / ${duration.label(reference: duration)}',
|
||||
'${clampedPosition.label(reference: duration)} / ${duration.label(reference: duration)}',
|
||||
style: const TextStyle(height: 1.0, fontSize: 12.0, color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:mangayomi/modules/anime/widgets/indicator_builder.dart';
|
|||
import 'package:mangayomi/modules/anime/widgets/subtitle_view.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/anime/widgets/play_or_pause_button.dart';
|
||||
import 'package:volume_controller/volume_controller.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -884,74 +885,6 @@ class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
|
|||
}
|
||||
}
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
||||
/// A material design play/pause button.
|
||||
class CustomMaterialPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoController controller;
|
||||
|
||||
const CustomMaterialPlayOrPauseButton({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
CustomMaterialPlayOrPauseButtonState createState() =>
|
||||
CustomMaterialPlayOrPauseButtonState();
|
||||
}
|
||||
|
||||
class CustomMaterialPlayOrPauseButtonState
|
||||
extends State<CustomMaterialPlayOrPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animation = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.controller.player.state.playing ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
StreamSubscription<bool>? subscription;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
subscription ??= widget.controller.player.stream.playing.listen((event) {
|
||||
if (event) {
|
||||
animation.forward();
|
||||
} else {
|
||||
animation.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animation.dispose();
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: widget.controller.player.playOrPause,
|
||||
iconSize: 65,
|
||||
color: Colors.white,
|
||||
icon: IgnorePointer(
|
||||
child: AnimatedIcon(
|
||||
progress: animation,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: 65,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> mobilePrimaryButtonBar(
|
||||
BuildContext context,
|
||||
GlobalKey<VideoState> key,
|
||||
|
|
@ -985,7 +918,7 @@ List<Widget> mobilePrimaryButtonBar(
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CustomMaterialPlayOrPauseButton(controller: controller),
|
||||
CustomPlayOrPauseButton(controller: controller, isDesktop: false),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed:
|
||||
|
|
|
|||
76
lib/modules/anime/widgets/play_or_pause_button.dart
Normal file
76
lib/modules/anime/widgets/play_or_pause_button.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
||||
/// A material design play/pause button.
|
||||
class CustomPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoController controller;
|
||||
final bool isDesktop;
|
||||
|
||||
const CustomPlayOrPauseButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.isDesktop,
|
||||
});
|
||||
|
||||
@override
|
||||
CustomPlayOrPauseButtonState createState() => CustomPlayOrPauseButtonState();
|
||||
}
|
||||
|
||||
class CustomPlayOrPauseButtonState extends State<CustomPlayOrPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animation = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.controller.player.state.playing ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
StreamSubscription<bool>? subscription;
|
||||
|
||||
double get iconSize => widget.isDesktop ? 25 : 65;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
subscription ??= widget.controller.player.stream.playing.listen((event) {
|
||||
if (event) {
|
||||
animation.forward();
|
||||
} else {
|
||||
animation.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animation.dispose();
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: widget.controller.player.playOrPause,
|
||||
iconSize: iconSize,
|
||||
color: Colors.white,
|
||||
icon: IgnorePointer(
|
||||
child: AnimatedIcon(
|
||||
progress: animation,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: iconSize,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue