Fix Fullscreen mode

This commit is contained in:
NBA2K1 2025-06-27 08:35:01 +02:00
parent e9dfb516ab
commit d21dbbbc6b
3 changed files with 220 additions and 190 deletions

View file

@ -56,9 +56,6 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
bool desktopFullScreenPlayer = false; bool desktopFullScreenPlayer = false;
@override @override
void dispose() { void dispose() {
if (_isDesktop) {
setFullScreen(value: desktopFullScreenPlayer);
}
for (var infoHash in _infoHashList) { for (var infoHash in _infoHashList) {
MTorrentServer().removeTorrent(infoHash); MTorrentServer().removeTorrent(infoHash);
} }
@ -169,6 +166,9 @@ class AnimeStreamPage extends riv.ConsumerStatefulWidget {
enum _AniSkipPhase { none, opening, ending } enum _AniSkipPhase { none, opening, ending }
/// When the user first opens a video (on Desktop).
bool _firstTime = true;
class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late final GlobalKey<VideoState> _key = GlobalKey<VideoState>(); late final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
@ -196,7 +196,6 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
), ),
); );
final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0); final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
final ValueNotifier<bool> _enterFullScreen = ValueNotifier(false);
final ValueNotifier<bool> _isDoubleSpeed = ValueNotifier(false); final ValueNotifier<bool> _isDoubleSpeed = ValueNotifier(false);
late final ValueNotifier<Duration> _currentPosition = ValueNotifier( late final ValueNotifier<Duration> _currentPosition = ValueNotifier(
_streamController.geTCurrentPosition(), _streamController.geTCurrentPosition(),
@ -235,13 +234,16 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
} }
} }
// If the last episode of an Anime has ended, exit fullscreen mode // If the last episode of an Anime has ended, exit fullscreen mode
if (!hasNextEpisode && val && _isDesktop && !_enterFullScreen.value) { final isFullScreen = ref.read(fullscreenProvider);
if (!hasNextEpisode && val && _isDesktop && isFullScreen) {
setFullScreen(value: false); setFullScreen(value: false);
ref.read(fullscreenProvider.notifier).state = false;
widget.desktopFullScreenPlayer.call(false);
} }
}); });
void pushToNewEpisode(BuildContext context, Chapter episode) { void pushToNewEpisode(BuildContext context, Chapter episode) {
widget.desktopFullScreenPlayer.call(true); widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
if (context.mounted) { if (context.mounted) {
pushReplacementMangaReaderView(context: context, chapter: episode); pushReplacementMangaReaderView(context: context, chapter: episode);
} }
@ -305,12 +307,24 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (_isDesktop) { // If player is being launched the first time,
setFullScreen(value: ref.read(fullScreenPlayerStateProvider)); // use global "Use Fullscreen" setting.
// Else (if user already watches an episode and just changes it),
// stay in the same mode, the user left it in.
if (_isDesktop && _firstTime) {
final globalFullscreen = ref.read(fullScreenPlayerStateProvider);
setFullScreen(value: globalFullscreen);
Future.microtask(() {
ref.read(fullscreenProvider.notifier).state = globalFullscreen;
widget.desktopFullScreenPlayer.call(globalFullscreen);
});
_firstTime = false;
} }
_currentPositionSub = _player.stream.position.listen( _currentPositionSub = _player.stream.position.listen(
_unifiedPositionHandler, _unifiedPositionHandler,
); );
_completed;
_currentTotalDurationSub;
_loadAndroidFont().then((_) { _loadAndroidFont().then((_) {
_player.open( _player.open(
Media( Media(
@ -979,6 +993,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
/// helper method for _mobileBottomButtonBar() and _desktopBottomButtonBar() /// helper method for _mobileBottomButtonBar() and _desktopBottomButtonBar()
Widget _buildSettingsButtons(BuildContext context) { Widget _buildSettingsButtons(BuildContext context) {
final isFullscreen = ref.watch(fullscreenProvider);
return Row( return Row(
children: [ children: [
IconButton( IconButton(
@ -1009,20 +1024,19 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}, },
), ),
if (_isDesktop) if (_isDesktop)
CustomMaterialDesktopFullscreenButton(controller: _controller) CustomMaterialDesktopFullscreenButton(
controller: _controller,
desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
)
else else
ValueListenableBuilder<bool>( IconButton(
valueListenable: _enterFullScreen, icon: Icon(isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen),
builder: (context, snapshot, _) { iconSize: 25,
return IconButton( color: Colors.white,
onPressed: () { onPressed: () {
_setLandscapeMode(!snapshot); _setLandscapeMode(!isFullscreen);
_enterFullScreen.value = !snapshot; ref.read(fullscreenProvider.notifier).state = !isFullscreen;
}, widget.desktopFullScreenPlayer.call(!isFullscreen);
icon: Icon(snapshot ? Icons.fullscreen_exit : Icons.fullscreen),
iconSize: 25,
color: Colors.white,
);
}, },
), ),
], ],
@ -1030,163 +1044,158 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
} }
Widget _topButtonBar(BuildContext context) { Widget _topButtonBar(BuildContext context) {
return ValueListenableBuilder<bool>( final fullScreen = ref.watch(fullscreenProvider);
valueListenable: _enterFullScreen, return Padding(
builder: (context, fullScreen, _) { padding: EdgeInsets.only(
return Padding( top: !_isDesktop && !fullScreen
padding: EdgeInsets.only( ? MediaQuery.of(context).padding.top
top: !_isDesktop && !fullScreen : 0,
? MediaQuery.of(context).padding.top ),
: 0, child: Row(
children: [
BackButton(
color: Colors.white,
onPressed: () {
if (_isDesktop && fullScreen) {
setFullScreen(value: !fullScreen);
ref.read(fullscreenProvider.notifier).state = !fullScreen;
widget.desktopFullScreenPlayer.call(!fullScreen);
} else {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
}
if (mounted) {
// Set variable to true, so the player uses the global
// "Use Fullscreen" setting again.
_firstTime = true;
Navigator.pop(context);
}
},
), ),
child: Row( Flexible(
child: ListTile(
dense: true,
title: SizedBox(
width: context.width(0.8),
child: Text(
widget.episode.manga.value!.name!,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
subtitle: SizedBox(
width: context.width(0.8),
child: Text(
widget.episode.name!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white.withValues(alpha: 0.7),
),
overflow: TextOverflow.ellipsis,
),
),
),
),
Flexible(
fit: FlexFit.tight,
child: ValueListenableBuilder<bool>(
valueListenable: _isDoubleSpeed,
builder: (context, snapshot, _) {
return Text.rich(
TextSpan(
children: snapshot
? [
WidgetSpan(child: Icon(Icons.fast_forward)),
TextSpan(text: " 2X"),
]
: [],
),
);
},
),
),
Row(
children: [ children: [
BackButton( btnToShowChapterListDialog(
color: Colors.white, context,
onPressed: () async { context.l10n.episodes,
if (_isDesktop) { widget.episode,
if (fullScreen) { onChanged: (v) {
setFullScreen(value: false); if (v) {
} else { _player.play();
if (mounted) {
Navigator.pop(context);
}
}
} else { } else {
SystemChrome.setEnabledSystemUIMode( _player.pause();
SystemUiMode.manual, }
overlays: SystemUiOverlay.values, },
); iconColor: Colors.white,
if (mounted) { ),
Navigator.pop(context); btnToShowShareScreenshot(
} widget.episode,
onChanged: (v) {
if (v) {
_player.play();
} else {
_player.pause();
} }
}, },
), ),
Flexible( // IconButton(
child: ListTile( // onPressed: () {
dense: true, // showDialog(
title: SizedBox( // context: context,
width: context.width(0.8), // builder: (context) {
child: Text( // return AlertDialog(
widget.episode.manga.value!.name!, // scrollable: true,
style: const TextStyle( // title: Text("Player Settings"),
fontWeight: FontWeight.bold, // content: SizedBox(
color: Colors.white, // width: context.width(0.8),
), // child: Column(
overflow: TextOverflow.ellipsis, // crossAxisAlignment:
), // CrossAxisAlignment.start,
), // children: [
subtitle: SizedBox( // SwitchListTile(
width: context.width(0.8), // value: false,
child: Text( // title: Text(
widget.episode.name!, // "Enable Volume and Brightness Gestures",
style: TextStyle( // style: TextStyle(
fontSize: 12, // color: Theme.of(context)
fontWeight: FontWeight.w400, // .textTheme
color: Colors.white.withValues(alpha: 0.7), // .bodyLarge!
), // .color!
overflow: TextOverflow.ellipsis, // .withValues(alpha: 0.9),
), // fontSize: 14),
), // ),
), // onChanged: (value) {}),
), // SwitchListTile(
Flexible( // value: false,
fit: FlexFit.tight, // title: Text(
child: ValueListenableBuilder<bool>( // "Enable Horizonal Seek Gestures",
valueListenable: _isDoubleSpeed, // style: TextStyle(
builder: (context, snapshot, _) { // color: Theme.of(context)
return Text.rich( // .textTheme
TextSpan( // .bodyLarge!
children: snapshot // .color!
? [ // .withValues(alpha: 0.9),
WidgetSpan(child: Icon(Icons.fast_forward)), // fontSize: 14),
TextSpan(text: " 2X"), // ),
] // onChanged: (value) {}),
: [], // ],
), // ),
); // ),
}, // );
), // });
), // },
Row( // icon: Icon(Icons.adaptive.more))
children: [
btnToShowChapterListDialog(
context,
context.l10n.episodes,
widget.episode,
onChanged: (v) {
if (v) {
_player.play();
} else {
_player.pause();
}
},
iconColor: Colors.white,
),
btnToShowShareScreenshot(
widget.episode,
onChanged: (v) {
if (v) {
_player.play();
} else {
_player.pause();
}
},
),
// IconButton(
// onPressed: () {
// showDialog(
// context: context,
// builder: (context) {
// return AlertDialog(
// scrollable: true,
// title: Text("Player Settings"),
// content: SizedBox(
// width: context.width(0.8),
// child: Column(
// crossAxisAlignment:
// CrossAxisAlignment.start,
// children: [
// SwitchListTile(
// value: false,
// title: Text(
// "Enable Volume and Brightness Gestures",
// style: TextStyle(
// color: Theme.of(context)
// .textTheme
// .bodyLarge!
// .color!
// .withValues(alpha: 0.9),
// fontSize: 14),
// ),
// onChanged: (value) {}),
// SwitchListTile(
// value: false,
// title: Text(
// "Enable Horizonal Seek Gestures",
// style: TextStyle(
// color: Theme.of(context)
// .textTheme
// .bodyLarge!
// .color!
// .withValues(alpha: 0.9),
// fontSize: 14),
// ),
// onChanged: (value) {}),
// ],
// ),
// ),
// );
// });
// },
// icon: Icon(Icons.adaptive.more))
],
),
], ],
), ),
); ],
}, ),
); );
} }
@ -1235,6 +1244,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
_isDoubleSpeed.value = value ?? false; _isDoubleSpeed.value = value ?? false;
}, },
defaultSkipIntroLength: skipIntroLength, defaultSkipIntroLength: skipIntroLength,
desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
) )
: MobileControllerWidget( : MobileControllerWidget(
videoController: _controller, videoController: _controller,

View file

@ -1,3 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/changed.dart';
@ -14,6 +15,8 @@ import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'anime_player_controller_provider.g.dart'; part 'anime_player_controller_provider.g.dart';
final fullscreenProvider = StateProvider<bool>((ref) => false);
@riverpod @riverpod
class AnimeStreamController extends _$AnimeStreamController { class AnimeStreamController extends _$AnimeStreamController {
@override @override

View file

@ -13,7 +13,7 @@ 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/extensions/duration.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class DesktopControllerWidget extends StatefulWidget { class DesktopControllerWidget extends ConsumerStatefulWidget {
final Function(Duration?) tempDuration; final Function(Duration?) tempDuration;
final Function(bool?) doubleSpeed; final Function(bool?) doubleSpeed;
final AnimeStreamController streamController; final AnimeStreamController streamController;
@ -23,6 +23,7 @@ class DesktopControllerWidget extends StatefulWidget {
final Widget bottomButtonBarWidget; final Widget bottomButtonBarWidget;
final Widget seekToWidget; final Widget seekToWidget;
final int defaultSkipIntroLength; final int defaultSkipIntroLength;
final void Function(bool) desktopFullScreenPlayer;
const DesktopControllerWidget({ const DesktopControllerWidget({
super.key, super.key,
required this.videoController, required this.videoController,
@ -34,14 +35,16 @@ class DesktopControllerWidget extends StatefulWidget {
required this.tempDuration, required this.tempDuration,
required this.doubleSpeed, required this.doubleSpeed,
required this.defaultSkipIntroLength, required this.defaultSkipIntroLength,
required this.desktopFullScreenPlayer,
}); });
@override @override
State<DesktopControllerWidget> createState() => ConsumerState<DesktopControllerWidget> createState() =>
_DesktopControllerWidgetState(); _DesktopControllerWidgetState();
} }
class _DesktopControllerWidgetState extends State<DesktopControllerWidget> { class _DesktopControllerWidgetState
extends ConsumerState<DesktopControllerWidget> {
bool mount = true; bool mount = true;
bool visible = true; bool visible = true;
bool cursorVisible = true; bool cursorVisible = true;
@ -201,9 +204,13 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
final volume = widget.videoController.player.state.volume - 5.0; final volume = widget.videoController.player.state.volume - 5.0;
widget.videoController.player.setVolume(volume.clamp(0.0, 100.0)); widget.videoController.player.setVolume(volume.clamp(0.0, 100.0));
}, },
const SingleActivator(LogicalKeyboardKey.keyF): () => setFullScreen(), const SingleActivator(LogicalKeyboardKey.keyF): () async {
const SingleActivator(LogicalKeyboardKey.escape): () => await _changeFullScreen(ref, widget.desktopFullScreenPlayer);
setFullScreen(value: false), },
const SingleActivator(LogicalKeyboardKey.escape): () async {
final desktopFullScreenPlayer = widget.desktopFullScreenPlayer;
await _changeFullScreen(ref, desktopFullScreenPlayer, value: false);
},
}, },
child: Stack( child: Stack(
children: [ children: [
@ -262,12 +269,13 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
}, },
onTapUp: !toggleFullscreenOnDoublePress onTapUp: !toggleFullscreenOnDoublePress
? null ? null
: (e) { : (e) async {
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(last); final difference = now.difference(last);
last = now; last = now;
if (difference < const Duration(milliseconds: 400)) { if (difference < const Duration(milliseconds: 400)) {
setFullScreen(); final fullScreen = widget.desktopFullScreenPlayer;
await _changeFullScreen(ref, fullScreen);
} }
}, },
onPanUpdate: modifyVolumeOnScroll onPanUpdate: modifyVolumeOnScroll
@ -489,6 +497,16 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
} }
} }
Future<void> _changeFullScreen(
WidgetRef ref,
void Function(bool) setFullScreenCallback, {
bool? value,
}) async {
final isFullScreen = await setFullScreen(value: value);
ref.read(fullscreenProvider.notifier).state = isFullScreen;
setFullScreenCallback(isFullScreen);
}
// BUTTON: VOLUME // BUTTON: VOLUME
/// MaterialDesktop design volume button & slider. /// MaterialDesktop design volume button & slider.
@ -751,36 +769,35 @@ class _CustomTrackShape extends RoundedRectSliderTrackShape {
} }
} }
class CustomMaterialDesktopFullscreenButton extends StatefulWidget { class CustomMaterialDesktopFullscreenButton extends ConsumerStatefulWidget {
final VideoController controller; final VideoController controller;
final void Function(bool) desktopFullScreenPlayer;
const CustomMaterialDesktopFullscreenButton({ const CustomMaterialDesktopFullscreenButton({
super.key, super.key,
required this.controller, required this.controller,
required this.desktopFullScreenPlayer,
}); });
@override @override
State<CustomMaterialDesktopFullscreenButton> createState() => ConsumerState<CustomMaterialDesktopFullscreenButton> createState() =>
_CustomMaterialDesktopFullscreenButtonState(); _CustomMaterialDesktopFullscreenButtonState();
} }
class _CustomMaterialDesktopFullscreenButtonState class _CustomMaterialDesktopFullscreenButtonState
extends State<CustomMaterialDesktopFullscreenButton> { extends ConsumerState<CustomMaterialDesktopFullscreenButton> {
bool _isFullscreen = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isFullScreen = ref.watch(fullscreenProvider);
return IconButton( return IconButton(
onPressed: () async { icon: isFullScreen
final isFullScreen = await setFullScreen();
setState(() {
_isFullscreen = isFullScreen;
});
},
icon: _isFullscreen
? const Icon(Icons.fullscreen_exit) ? const Icon(Icons.fullscreen_exit)
: const Icon(Icons.fullscreen), : const Icon(Icons.fullscreen),
iconSize: 25, iconSize: 25,
color: Colors.white, color: Colors.white,
onPressed: () async {
await _changeFullScreen(ref, widget.desktopFullScreenPlayer);
},
); );
} }
} }
@ -799,5 +816,5 @@ Future<bool> setFullScreen({bool? value}) async {
} else { } else {
await windowManager.setFullScreen(false); await windowManager.setFullScreen(false);
} }
return isFullScreen; return !isFullScreen;
} }