Merge pull request #457 from NBA2K1/main

Improve Video Playback UI & Maintainability
This commit is contained in:
Moustapha Kodjo Amadou 2025-05-19 15:29:36 +01:00 committed by GitHub
commit fc5776ab60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 148 additions and 245 deletions

View file

@ -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/

View file

@ -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,

View file

@ -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(

View file

@ -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),
);
}

View file

@ -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:

View 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,
),
),
);
}
}