From 8e2ce7861bf9c860b361671b3a73180e7a6e4c03 Mon Sep 17 00:00:00 2001 From: PartyDonut <42371342+PartyDonut@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:07:23 +0200 Subject: [PATCH] feature(web): Added full-screen button and volume slider (#50) ## Pull Request Description Adds the full screen toggle to web and the volume slider. fix: small fixes for desktop padding fix: only reload widgets when the content has changed ## Issue Being Fixed Issue Number: #28 --------- Co-authored-by: PartyDonut --- .../video_player_settings_provider.dart | 19 +- lib/screens/video_player/video_player.dart | 32 +- .../video_player/video_player_controls.dart | 459 +++++++++--------- .../components/navigation_body.dart | 3 +- .../components/navigation_drawer.dart | 3 +- lib/widgets/shared/full_screen_button.dart | 44 ++ .../shared/full_screen_button_web.dart | 49 ++ 7 files changed, 359 insertions(+), 250 deletions(-) create mode 100644 lib/widgets/shared/full_screen_button.dart create mode 100644 lib/widgets/shared/full_screen_button_web.dart diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index 0acd3cf..2c297f3 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -1,9 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:screen_brightness/screen_brightness.dart'; + import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:screen_brightness/screen_brightness.dart'; final videoPlayerSettingsProvider = StateNotifierProvider((ref) { @@ -51,7 +53,14 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(videoFit: value); - void setVolume(double value) => state = state.copyWith(internalVolume: value); + void setVolume(double value) { + state = state.copyWith(internalVolume: value); + ref.read(videoPlayerProvider).setVolume(value); + } - void steppedVolume(int i) => state = state.copyWith(internalVolume: (state.volume + i).clamp(0, 100)); + void steppedVolume(int i) { + final value = (state.volume + i).clamp(0, 100).toDouble(); + state = state.copyWith(internalVolume: value); + ref.read(videoPlayerProvider).setVolume(value); + } } diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index c964450..d5592f7 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -1,14 +1,15 @@ import 'dart:async'; -import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/providers/settings/video_player_settings_provider.dart'; -import 'package:fladder/screens/video_player/video_player_controls.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:media_kit_video/media_kit_video.dart'; +import 'package:fladder/models/media_playback_model.dart'; +import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/screens/video_player/video_player_controls.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/themes_data.dart'; @@ -28,7 +29,7 @@ class _VideoPlayerState extends ConsumerState with WidgetsBindingOb @override void didChangeAppLifecycleState(AppLifecycleState state) { //Don't pause on desktop focus loss - if (!AdaptiveLayout.of(context).isDesktop) { + if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) { switch (state) { case AppLifecycleState.resumed: if (playing) ref.read(videoPlayerProvider).play(); @@ -62,14 +63,11 @@ class _VideoPlayerState extends ConsumerState with WidgetsBindingOb @override Widget build(BuildContext context) { - final playerProvider = ref.watch(videoPlayerProvider); - ref.listen(videoPlayerSettingsProvider.select((value) => value.volume), (previous, next) { - playerProvider.setVolume(next); - }); - final videoPlayerSettings = ref.watch(videoPlayerSettingsProvider); + final fillScreen = ref.watch(videoPlayerSettingsProvider.select((value) => value.fillScreen)); + final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit)); final padding = MediaQuery.of(context).padding; - final playerController = playerProvider.controller; + final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller)); return Material( color: Colors.black, @@ -95,20 +93,16 @@ class _VideoPlayerState extends ConsumerState with WidgetsBindingOb children: [ if (playerController != null) Padding( - padding: videoPlayerSettings.fillScreen - ? EdgeInsets.zero - : EdgeInsets.only(left: padding.left, right: padding.right), + padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right), child: OrientationBuilder(builder: (context, orientation) { return Video( - key: Key("$videoPlayerSettings|$orientation"), + key: Key(orientation.toString()), controller: playerController, fill: Colors.transparent, wakelock: true, - fit: videoPlayerSettings.fillScreen - ? (MediaQuery.of(context).orientation == Orientation.portrait - ? videoPlayerSettings.videoFit - : BoxFit.cover) - : videoPlayerSettings.videoFit, + fit: fillScreen + ? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover) + : videoFit, subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false), controls: NoVideoControls, ); diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 84c2e61..09641d1 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -11,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'package:universal_html/html.dart' as html; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/items/intro_skip_model.dart'; @@ -31,6 +31,8 @@ import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; +import 'package:fladder/widgets/shared/full_screen_button.dart' + if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; class DesktopControls extends ConsumerStatefulWidget { const DesktopControls({super.key}); @@ -50,20 +52,13 @@ class _DesktopControlsState extends ConsumerState { @override Widget build(BuildContext context) { - final mediaPlayback = ref.watch(mediaPlaybackProvider); final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel)); - final player = ref.watch(videoPlayerProvider); - bool showIntroSkipButton = introSkipModel?.introInRange(mediaPlayback.position) ?? false; - bool showCreditSkipButton = introSkipModel?.creditsInRange(mediaPlayback.position) ?? false; + final player = ref.watch(videoPlayerProvider.select((value) => value.controller)); if (AdaptiveLayout.of(context).isDesktop) { focusNode.requestFocus(); } - return Listener( - onPointerSignal: (event) { - log('Timer reset'); - resetTimer(); - }, + onPointerSignal: (event) => resetTimer(), child: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { @@ -75,6 +70,9 @@ class _DesktopControlsState extends ConsumerState { focusNode: focusNode, autofocus: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer, onKeyEvent: (value) { + final position = ref.read(mediaPlaybackProvider).position; + bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; + bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; if (value is KeyRepeatEvent) {} if (value is KeyDownEvent) { if (value.logicalKey == LogicalKeyboardKey.keyS) { @@ -92,10 +90,10 @@ class _DesktopControlsState extends ConsumerState { ref.read(videoPlayerProvider).playOrPause(); } if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { - seekBack(mediaPlayback); + seekBack(ref); } if (value.logicalKey == LogicalKeyboardKey.arrowRight) { - seekForward(mediaPlayback); + seekForward(ref); } if (value.logicalKey == LogicalKeyboardKey.keyF) { toggleFullScreen(); @@ -122,13 +120,18 @@ class _DesktopControlsState extends ConsumerState { onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null, child: Stack( children: [ - if (player.controller != null) + if (player != null) VideoSubtitles( key: const Key('subtitles'), - controller: player.controller!, + controller: player, overlayed: showOverlay, ), - if (AdaptiveLayout.of(context).isDesktop) playButton(mediaPlayback), + if (AdaptiveLayout.of(context).isDesktop) + Consumer(builder: (context, ref, child) { + final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing)); + final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering)); + return playButton(playing, buffering); + }), IgnorePointer( ignoring: !showOverlay, child: AnimatedOpacity( @@ -138,33 +141,44 @@ class _DesktopControlsState extends ConsumerState { children: [ topButtons(context), const Spacer(), - bottomButtons(context, mediaPlayback), + bottomButtons(context), ], ), ), ), - if (showIntroSkipButton) - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(32), - child: IntroSkipButton( - isOverlayVisible: showOverlay, - skipIntro: () => skipIntro(introSkipModel), - ), - ), - ), - if (showCreditSkipButton) - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(32), - child: CreditsSkipButton( - isOverlayVisible: showOverlay, - skipCredits: () => skipCredits(introSkipModel), - ), - ), - ) + Consumer( + builder: (context, ref, child) { + final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); + bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; + bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; + return Stack( + children: [ + if (showIntroSkipButton) + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(32), + child: IntroSkipButton( + isOverlayVisible: showOverlay, + skipIntro: () => skipIntro(introSkipModel), + ), + ), + ), + if (showCreditSkipButton) + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(32), + child: CreditsSkipButton( + isOverlayVisible: showOverlay, + skipCredits: () => skipCredits(introSkipModel), + ), + ), + ) + ], + ); + }, + ), ], ), ), @@ -174,14 +188,14 @@ class _DesktopControlsState extends ConsumerState { ); } - Widget playButton(MediaPlaybackModel mediaPlayback) { + Widget playButton(bool playing, bool buffering) { return Align( alignment: Alignment.center, child: AnimatedScale( curve: Curves.easeInOutCubicEmphasized, - scale: mediaPlayback.playing + scale: playing ? 0 - : mediaPlayback.buffering + : buffering ? 0 : 1, duration: const Duration(milliseconds: 250), @@ -211,44 +225,40 @@ class _DesktopControlsState extends ConsumerState { child: Stack( children: [ if (AdaptiveLayout.of(context).isDesktop) - const Flexible( - child: Align( - alignment: Alignment.topRight, - child: DefaultTitleBar(), - ), + const Align( + alignment: Alignment.topRight, + child: DefaultTitleBar(), ), - Flexible( - child: Padding( - padding: MediaQuery.paddingOf(context).copyWith(bottom: 0), - child: Container( - alignment: Alignment.topCenter, - height: 80, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () => minimizePlayer(context), - icon: const Icon( - IconsaxOutline.arrow_down_1, - size: 24, - ), + Padding( + padding: MediaQuery.paddingOf(context).copyWith(bottom: 0), + child: Container( + alignment: Alignment.topCenter, + height: 80, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () => minimizePlayer(context), + icon: const Icon( + IconsaxOutline.arrow_down_1, + size: 24, ), - const SizedBox(width: 16), - Flexible( - child: Text( - currentItem?.title ?? "", - style: Theme.of(context).textTheme.titleLarge, - ), + ), + const SizedBox(width: 16), + Flexible( + child: Text( + currentItem?.title ?? "", + style: Theme.of(context).textTheme.titleLarge, ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), @@ -257,147 +267,140 @@ class _DesktopControlsState extends ConsumerState { ); } - Widget bottomButtons(BuildContext context, MediaPlaybackModel mediaPlayback) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withOpacity(0.8), - Colors.black.withOpacity(0), - ], - )), - child: Padding( - padding: MediaQuery.paddingOf(context).add( - const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: progressBar(mediaPlayback), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - flex: 2, - child: Row( - children: [ - IconButton( - onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), - icon: const Icon(IconsaxOutline.more)), - if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[ + Widget bottomButtons(BuildContext context) { + return Consumer(builder: (context, ref, child) { + final mediaPlayback = ref.watch(mediaPlaybackProvider); + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.8), + Colors.black.withOpacity(0), + ], + )), + child: Padding( + padding: MediaQuery.paddingOf(context).add( + const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: progressBar(mediaPlayback), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + flex: 2, + child: Row( + children: [ IconButton( - onPressed: () => showSubSelection(context), - icon: const Icon(IconsaxOutline.subtitle), - ), - IconButton( - onPressed: () => showAudioSelection(context), - icon: const Icon(IconsaxOutline.audio_square), - ), - ], - if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[ - Flexible( - child: ElevatedButton.icon( + onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), + icon: const Icon(IconsaxOutline.more)), + if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[ + IconButton( onPressed: () => showSubSelection(context), icon: const Icon(IconsaxOutline.subtitle), - label: Text( - ref - .watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream)) - ?.language - .capitalize() ?? - "Off", - maxLines: 1, - ), ), - ), - Flexible( - child: ElevatedButton.icon( + IconButton( onPressed: () => showAudioSelection(context), icon: const Icon(IconsaxOutline.audio_square), - label: Text( - ref - .watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream)) - ?.language - .capitalize() ?? - "Off", - maxLines: 1, + ), + ], + if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[ + Flexible( + child: ElevatedButton.icon( + onPressed: () => showSubSelection(context), + icon: const Icon(IconsaxOutline.subtitle), + label: Text( + ref + .watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream)) + ?.language + .capitalize() ?? + "Off", + maxLines: 1, + ), ), ), - ) - ], - ].addInBetween(const SizedBox( - width: 4, - )), - ), - ), - previousButton, - seekBackwardButton(mediaPlayback), - IconButton.filledTonal( - iconSize: 38, - onPressed: () { - ref.read(videoPlayerProvider).playOrPause(); - }, - icon: Icon( - mediaPlayback.playing ? IconsaxBold.pause : IconsaxBold.play, - ), - ), - seekForwardButton(mediaPlayback), - nextVideoButton, - Flexible( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Tooltip( - message: "Stop", - child: IconButton(onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.stop))), - const Spacer(), - if (AdaptiveLayout.of(context).isDesktop && ref.read(videoPlayerProvider).player != null) ...{ - // OpenQueueButton(x), - // ChapterButton( - // position: position, - // player: ref.read(videoPlayerProvider).player!, - // ), - Listener( - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy > 0) { - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); - } else { - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); - } - } - }, - child: VideoVolumeSlider( - onChanged: () => resetTimer(), - ), - ), - FutureBuilder( - future: windowManager.isFullScreen(), - builder: (context, snapshot) { - final isFullScreen = snapshot.data ?? true; - return IconButton( - onPressed: () => windowManager.setFullScreen(!isFullScreen), - icon: Icon( - isFullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, + Flexible( + child: ElevatedButton.icon( + onPressed: () => showAudioSelection(context), + icon: const Icon(IconsaxOutline.audio_square), + label: Text( + ref + .watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream)) + ?.language + .capitalize() ?? + "Off", + maxLines: 1, ), - ); - }, - ), - } - ].addInBetween(const SizedBox(width: 8)), + ), + ) + ], + ].addInBetween(const SizedBox( + width: 4, + )), + ), ), - ), - ].addInBetween(const SizedBox(width: 6)), - ), - ], + previousButton, + seekBackwardButton(ref), + IconButton.filledTonal( + iconSize: 38, + onPressed: () { + ref.read(videoPlayerProvider).playOrPause(); + }, + icon: Icon( + mediaPlayback.playing ? IconsaxBold.pause : IconsaxBold.play, + ), + ), + seekForwardButton(ref), + nextVideoButton, + Flexible( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Tooltip( + message: "Stop", + child: IconButton(onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.stop))), + const Spacer(), + if ((AdaptiveLayout.of(context).isDesktop || kIsWeb) && + ref.read(videoPlayerProvider).player != null) ...{ + // OpenQueueButton(x), + // ChapterButton( + // position: position, + // player: ref.read(videoPlayerProvider).player!, + // ), + Listener( + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); + } else { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); + } + } + }, + child: VideoVolumeSlider( + onChanged: () => resetTimer(), + ), + ), + const FullScreenButton(), + } + ].addInBetween(const SizedBox(width: 8)), + ), + ), + ].addInBetween(const SizedBox(width: 6)), + ), + ], + ), ), - ), - ); + ); + }); } Widget progressBar(MediaPlaybackModel mediaPlayback) { @@ -534,9 +537,9 @@ class _DesktopControlsState extends ConsumerState { ); } - Widget seekBackwardButton(MediaPlaybackModel mediaPlaybackModel) { + Widget seekBackwardButton(WidgetRef ref) { return IconButton( - onPressed: () => seekBack(mediaPlaybackModel), + onPressed: () => seekBack(ref), tooltip: "-10", iconSize: 40, icon: const Icon( @@ -545,9 +548,9 @@ class _DesktopControlsState extends ConsumerState { ); } - Widget seekForwardButton(MediaPlaybackModel mediaPlaybackModel) { + Widget seekForwardButton(WidgetRef ref) { return IconButton( - onPressed: () => seekForward(mediaPlaybackModel), + onPressed: () => seekForward(ref), tooltip: "15", iconSize: 40, icon: const Stack( @@ -574,13 +577,15 @@ class _DesktopControlsState extends ConsumerState { } } - void seekBack(MediaPlaybackModel mediaPlayback, {int seconds = 15}) { + void seekBack(WidgetRef ref, {int seconds = 15}) { + final mediaPlayback = ref.read(mediaPlaybackProvider); resetTimer(); final newPosition = (mediaPlayback.position.inSeconds - seconds).clamp(0, mediaPlayback.duration.inSeconds); ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); } - void seekForward(MediaPlaybackModel mediaPlayback, {int seconds = 15}) { + void seekForward(WidgetRef ref, {int seconds = 15}) { + final mediaPlayback = ref.read(mediaPlaybackProvider); resetTimer(); final newPosition = (mediaPlayback.position.inSeconds + seconds).clamp(0, mediaPlayback.duration.inSeconds); ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); @@ -592,6 +597,7 @@ class _DesktopControlsState extends ConsumerState { ); void toggleOverlay({bool? value}) { + if (showOverlay == (value ?? !showOverlay)) return; setState(() => showOverlay = (value ?? !showOverlay)); resetTimer(); SystemChrome.setEnabledSystemUIMode(showOverlay ? SystemUiMode.edgeToEdge : SystemUiMode.leanBack, overlays: []); @@ -609,9 +615,17 @@ class _DesktopControlsState extends ConsumerState { Navigator.of(context).pop(); } + void resetTimer() => timer.reset(); + + Future closePlayer() async { + clearOverlaySettings(); + ref.read(videoPlayerProvider).stop(); + Navigator.of(context).pop(); + } + Future clearOverlaySettings() async { toggleOverlay(value: true); - if (!AdaptiveLayout.of(context).isDesktop) { + if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) { ScreenBrightness().resetScreenBrightness(); } else { disableFullscreen(); @@ -624,19 +638,18 @@ class _DesktopControlsState extends ConsumerState { timer.cancel(); } - void resetTimer() => timer.reset(); - - Future closePlayer() async { - clearOverlaySettings(); - ref.read(videoPlayerProvider).stop(); - Navigator.of(context).pop(); - } - Future disableFullscreen() async { resetTimer(); - final isFullScreen = await windowManager.isFullScreen(); - if (isFullScreen) { - await windowManager.setFullScreen(false); + if (kIsWeb) { + if (html.document.fullscreenElement != null) { + html.document.exitFullscreen(); + await Future.delayed(const Duration(milliseconds: 500)); + } + } else { + final isFullScreen = await windowManager.isFullScreen(); + if (isFullScreen) { + await windowManager.setFullScreen(false); + } } } diff --git a/lib/widgets/navigation_scaffold/components/navigation_body.dart b/lib/widgets/navigation_scaffold/components/navigation_body.dart index 6e0ad17..9003fe7 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_body.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_body.dart @@ -123,7 +123,8 @@ class _NavigationBodyState extends ConsumerState { Flexible( child: Padding( key: const Key('navigation_rail'), - padding: MediaQuery.paddingOf(context).copyWith(right: 0), + padding: + MediaQuery.paddingOf(context).copyWith(right: 0, top: AdaptiveLayout.of(context).isDesktop ? 8 : null), child: Column( children: [ IconButton( diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index 678ce36..12e8f74 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -41,9 +41,8 @@ class NestedNavigationDrawer extends ConsumerWidget { backgroundColor: isExpanded ? Colors.transparent : null, surfaceTintColor: isExpanded ? Colors.transparent : null, children: [ - if (AdaptiveLayout.of(context).isDesktop || kIsWeb) const SizedBox(height: 16), Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 0), + padding: EdgeInsets.fromLTRB(28, AdaptiveLayout.of(context).isDesktop ? 0 : 16, 16, 0), child: Row( children: [ Expanded( diff --git a/lib/widgets/shared/full_screen_button.dart b/lib/widgets/shared/full_screen_button.dart new file mode 100644 index 0000000..7d249fd --- /dev/null +++ b/lib/widgets/shared/full_screen_button.dart @@ -0,0 +1,44 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; + +import 'package:ficonsax/ficonsax.dart'; +import 'package:window_manager/window_manager.dart'; + +class FullScreenButton extends StatefulWidget { + const FullScreenButton({super.key}); + + @override + State createState() => _FullScreenButtonState(); +} + +class _FullScreenButtonState extends State { + bool isFullScreen = false; + + @override + void initState() { + super.initState(); + Future.microtask(checkFullScreen); + } + + void checkFullScreen() async { + final fullScreen = await windowManager.isFullScreen(); + setState(() { + isFullScreen = fullScreen; + }); + log(isFullScreen.toString()); + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () async { + await windowManager.setFullScreen(!isFullScreen); + checkFullScreen(); + }, + icon: Icon( + isFullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, + ), + ); + } +} diff --git a/lib/widgets/shared/full_screen_button_web.dart b/lib/widgets/shared/full_screen_button_web.dart new file mode 100644 index 0000000..af6ec6f --- /dev/null +++ b/lib/widgets/shared/full_screen_button_web.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:ficonsax/ficonsax.dart'; +import 'package:universal_html/html.dart' as html; + +class FullScreenButton extends StatefulWidget { + const FullScreenButton({super.key}); + + @override + State createState() => _FullScreenButtonState(); +} + +class _FullScreenButtonState extends State { + bool fullScreen = false; + + @override + void initState() { + super.initState(); + _updateFullScreenStatus(); + } + + void _updateFullScreenStatus() { + setState(() { + fullScreen = html.document.fullscreenElement != null; + }); + } + + void toggleFullScreen() async { + if (fullScreen) { + html.document.exitFullscreen(); + //Wait for 1 second + await Future.delayed(const Duration(seconds: 1)); + } else { + await html.document.documentElement?.requestFullscreen(); + } + + _updateFullScreenStatus(); + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: toggleFullScreen, + icon: Icon( + fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, + ), + ); + } +}