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 <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-10-19 17:07:23 +02:00 committed by GitHub
parent da9e0423c8
commit 8e2ce7861b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 359 additions and 250 deletions

View file

@ -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/models/settings/video_player_settings.dart';
import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/providers/video_player_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 = final videoPlayerSettingsProvider =
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) { StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
@ -51,7 +53,14 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value); void setFitType(BoxFit? value) => 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);
}
} }

View file

@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:fladder/models/media_playback_model.dart'; import 'package:flutter/foundation.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/video_player/video_player_controls.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit_video/media_kit_video.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/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/adaptive_layout.dart';
import 'package:fladder/util/themes_data.dart'; import 'package:fladder/util/themes_data.dart';
@ -28,7 +29,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
//Don't pause on desktop focus loss //Don't pause on desktop focus loss
if (!AdaptiveLayout.of(context).isDesktop) { if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) {
switch (state) { switch (state) {
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
if (playing) ref.read(videoPlayerProvider).play(); if (playing) ref.read(videoPlayerProvider).play();
@ -62,14 +63,11 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final playerProvider = ref.watch(videoPlayerProvider); final fillScreen = ref.watch(videoPlayerSettingsProvider.select((value) => value.fillScreen));
ref.listen(videoPlayerSettingsProvider.select((value) => value.volume), (previous, next) { final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit));
playerProvider.setVolume(next);
});
final videoPlayerSettings = ref.watch(videoPlayerSettingsProvider);
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
final playerController = playerProvider.controller; final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
return Material( return Material(
color: Colors.black, color: Colors.black,
@ -95,20 +93,16 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
children: [ children: [
if (playerController != null) if (playerController != null)
Padding( Padding(
padding: videoPlayerSettings.fillScreen padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
? EdgeInsets.zero
: EdgeInsets.only(left: padding.left, right: padding.right),
child: OrientationBuilder(builder: (context, orientation) { child: OrientationBuilder(builder: (context, orientation) {
return Video( return Video(
key: Key("$videoPlayerSettings|$orientation"), key: Key(orientation.toString()),
controller: playerController, controller: playerController,
fill: Colors.transparent, fill: Colors.transparent,
wakelock: true, wakelock: true,
fit: videoPlayerSettings.fillScreen fit: fillScreen
? (MediaQuery.of(context).orientation == Orientation.portrait ? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
? videoPlayerSettings.videoFit : videoFit,
: BoxFit.cover)
: videoPlayerSettings.videoFit,
subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false), subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false),
controls: NoVideoControls, controls: NoVideoControls,
); );

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -11,6 +10,7 @@ import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_brightness/screen_brightness.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:window_manager/window_manager.dart';
import 'package:fladder/models/items/intro_skip_model.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/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.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 { class DesktopControls extends ConsumerStatefulWidget {
const DesktopControls({super.key}); const DesktopControls({super.key});
@ -50,20 +52,13 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaPlayback = ref.watch(mediaPlaybackProvider);
final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel)); final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel));
final player = ref.watch(videoPlayerProvider); final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
bool showIntroSkipButton = introSkipModel?.introInRange(mediaPlayback.position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(mediaPlayback.position) ?? false;
if (AdaptiveLayout.of(context).isDesktop) { if (AdaptiveLayout.of(context).isDesktop) {
focusNode.requestFocus(); focusNode.requestFocus();
} }
return Listener( return Listener(
onPointerSignal: (event) { onPointerSignal: (event) => resetTimer(),
log('Timer reset');
resetTimer();
},
child: PopScope( child: PopScope(
canPop: false, canPop: false,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
@ -75,6 +70,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
focusNode: focusNode, focusNode: focusNode,
autofocus: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer, autofocus: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer,
onKeyEvent: (value) { 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 KeyRepeatEvent) {}
if (value is KeyDownEvent) { if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.keyS) { if (value.logicalKey == LogicalKeyboardKey.keyS) {
@ -92,10 +90,10 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
ref.read(videoPlayerProvider).playOrPause(); ref.read(videoPlayerProvider).playOrPause();
} }
if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { if (value.logicalKey == LogicalKeyboardKey.arrowLeft) {
seekBack(mediaPlayback); seekBack(ref);
} }
if (value.logicalKey == LogicalKeyboardKey.arrowRight) { if (value.logicalKey == LogicalKeyboardKey.arrowRight) {
seekForward(mediaPlayback); seekForward(ref);
} }
if (value.logicalKey == LogicalKeyboardKey.keyF) { if (value.logicalKey == LogicalKeyboardKey.keyF) {
toggleFullScreen(); toggleFullScreen();
@ -122,13 +120,18 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null, onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack( child: Stack(
children: [ children: [
if (player.controller != null) if (player != null)
VideoSubtitles( VideoSubtitles(
key: const Key('subtitles'), key: const Key('subtitles'),
controller: player.controller!, controller: player,
overlayed: showOverlay, 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( IgnorePointer(
ignoring: !showOverlay, ignoring: !showOverlay,
child: AnimatedOpacity( child: AnimatedOpacity(
@ -138,33 +141,44 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
children: [ children: [
topButtons(context), topButtons(context),
const Spacer(), const Spacer(),
bottomButtons(context, mediaPlayback), bottomButtons(context),
], ],
), ),
), ),
), ),
if (showIntroSkipButton) Consumer(
Align( builder: (context, ref, child) {
alignment: Alignment.centerRight, final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
child: Padding( bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
padding: const EdgeInsets.all(32), bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
child: IntroSkipButton( return Stack(
isOverlayVisible: showOverlay, children: [
skipIntro: () => skipIntro(introSkipModel), if (showIntroSkipButton)
), Align(
), alignment: Alignment.centerRight,
), child: Padding(
if (showCreditSkipButton) padding: const EdgeInsets.all(32),
Align( child: IntroSkipButton(
alignment: Alignment.centerRight, isOverlayVisible: showOverlay,
child: Padding( skipIntro: () => skipIntro(introSkipModel),
padding: const EdgeInsets.all(32), ),
child: CreditsSkipButton( ),
isOverlayVisible: showOverlay, ),
skipCredits: () => skipCredits(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<DesktopControls> {
); );
} }
Widget playButton(MediaPlaybackModel mediaPlayback) { Widget playButton(bool playing, bool buffering) {
return Align( return Align(
alignment: Alignment.center, alignment: Alignment.center,
child: AnimatedScale( child: AnimatedScale(
curve: Curves.easeInOutCubicEmphasized, curve: Curves.easeInOutCubicEmphasized,
scale: mediaPlayback.playing scale: playing
? 0 ? 0
: mediaPlayback.buffering : buffering
? 0 ? 0
: 1, : 1,
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
@ -211,44 +225,40 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
child: Stack( child: Stack(
children: [ children: [
if (AdaptiveLayout.of(context).isDesktop) if (AdaptiveLayout.of(context).isDesktop)
const Flexible( const Align(
child: Align( alignment: Alignment.topRight,
alignment: Alignment.topRight, child: DefaultTitleBar(),
child: DefaultTitleBar(),
),
), ),
Flexible( Padding(
child: Padding( padding: MediaQuery.paddingOf(context).copyWith(bottom: 0),
padding: MediaQuery.paddingOf(context).copyWith(bottom: 0), child: Container(
child: Container( alignment: Alignment.topCenter,
alignment: Alignment.topCenter, height: 80,
height: 80, child: Column(
child: Column( children: [
children: [ Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 12), child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ IconButton(
IconButton( onPressed: () => minimizePlayer(context),
onPressed: () => minimizePlayer(context), icon: const Icon(
icon: const Icon( IconsaxOutline.arrow_down_1,
IconsaxOutline.arrow_down_1, size: 24,
size: 24,
),
), ),
const SizedBox(width: 16), ),
Flexible( const SizedBox(width: 16),
child: Text( Flexible(
currentItem?.title ?? "", child: Text(
style: Theme.of(context).textTheme.titleLarge, currentItem?.title ?? "",
), style: Theme.of(context).textTheme.titleLarge,
), ),
], ),
), ],
), ),
], ),
), ],
), ),
), ),
), ),
@ -257,147 +267,140 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
} }
Widget bottomButtons(BuildContext context, MediaPlaybackModel mediaPlayback) { Widget bottomButtons(BuildContext context) {
return Container( return Consumer(builder: (context, ref, child) {
decoration: BoxDecoration( final mediaPlayback = ref.watch(mediaPlaybackProvider);
gradient: LinearGradient( return Container(
begin: Alignment.bottomCenter, decoration: BoxDecoration(
end: Alignment.topCenter, gradient: LinearGradient(
colors: [ begin: Alignment.bottomCenter,
Colors.black.withOpacity(0.8), end: Alignment.topCenter,
Colors.black.withOpacity(0), 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: Padding(
), padding: MediaQuery.paddingOf(context).add(
child: Column( const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12),
children: [ ),
Padding( child: Column(
padding: const EdgeInsets.symmetric(horizontal: 12), children: [
child: progressBar(mediaPlayback), Padding(
), padding: const EdgeInsets.symmetric(horizontal: 12),
const SizedBox(height: 16), child: progressBar(mediaPlayback),
Row( ),
mainAxisAlignment: MainAxisAlignment.center, const SizedBox(height: 16),
children: [ Row(
Flexible( mainAxisAlignment: MainAxisAlignment.center,
flex: 2, children: [
child: Row( Flexible(
children: <Widget>[ flex: 2,
IconButton( child: Row(
onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), children: <Widget>[
icon: const Icon(IconsaxOutline.more)),
if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[
IconButton( IconButton(
onPressed: () => showSubSelection(context), onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)),
icon: const Icon(IconsaxOutline.subtitle), icon: const Icon(IconsaxOutline.more)),
), if (AdaptiveLayout.layoutOf(context) == LayoutState.tablet) ...[
IconButton( IconButton(
onPressed: () => showAudioSelection(context),
icon: const Icon(IconsaxOutline.audio_square),
),
],
if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[
Flexible(
child: ElevatedButton.icon(
onPressed: () => showSubSelection(context), onPressed: () => showSubSelection(context),
icon: const Icon(IconsaxOutline.subtitle), icon: const Icon(IconsaxOutline.subtitle),
label: Text(
ref
.watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream))
?.language
.capitalize() ??
"Off",
maxLines: 1,
),
), ),
), IconButton(
Flexible(
child: ElevatedButton.icon(
onPressed: () => showAudioSelection(context), onPressed: () => showAudioSelection(context),
icon: const Icon(IconsaxOutline.audio_square), icon: const Icon(IconsaxOutline.audio_square),
label: Text( ),
ref ],
.watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream)) if (AdaptiveLayout.layoutOf(context) == LayoutState.desktop) ...[
?.language Flexible(
.capitalize() ?? child: ElevatedButton.icon(
"Off", onPressed: () => showSubSelection(context),
maxLines: 1, icon: const Icon(IconsaxOutline.subtitle),
label: Text(
ref
.watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream))
?.language
.capitalize() ??
"Off",
maxLines: 1,
),
), ),
), ),
) Flexible(
], child: ElevatedButton.icon(
].addInBetween(const SizedBox( onPressed: () => showAudioSelection(context),
width: 4, icon: const Icon(IconsaxOutline.audio_square),
)), label: Text(
), ref
), .watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream))
previousButton, ?.language
seekBackwardButton(mediaPlayback), .capitalize() ??
IconButton.filledTonal( "Off",
iconSize: 38, maxLines: 1,
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,
), ),
); ),
}, )
), ],
} ].addInBetween(const SizedBox(
].addInBetween(const SizedBox(width: 8)), width: 4,
)),
),
), ),
), previousButton,
].addInBetween(const SizedBox(width: 6)), 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) { Widget progressBar(MediaPlaybackModel mediaPlayback) {
@ -534,9 +537,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
} }
Widget seekBackwardButton(MediaPlaybackModel mediaPlaybackModel) { Widget seekBackwardButton(WidgetRef ref) {
return IconButton( return IconButton(
onPressed: () => seekBack(mediaPlaybackModel), onPressed: () => seekBack(ref),
tooltip: "-10", tooltip: "-10",
iconSize: 40, iconSize: 40,
icon: const Icon( icon: const Icon(
@ -545,9 +548,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
} }
Widget seekForwardButton(MediaPlaybackModel mediaPlaybackModel) { Widget seekForwardButton(WidgetRef ref) {
return IconButton( return IconButton(
onPressed: () => seekForward(mediaPlaybackModel), onPressed: () => seekForward(ref),
tooltip: "15", tooltip: "15",
iconSize: 40, iconSize: 40,
icon: const Stack( icon: const Stack(
@ -574,13 +577,15 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} }
} }
void seekBack(MediaPlaybackModel mediaPlayback, {int seconds = 15}) { void seekBack(WidgetRef ref, {int seconds = 15}) {
final mediaPlayback = ref.read(mediaPlaybackProvider);
resetTimer(); resetTimer();
final newPosition = (mediaPlayback.position.inSeconds - seconds).clamp(0, mediaPlayback.duration.inSeconds); final newPosition = (mediaPlayback.position.inSeconds - seconds).clamp(0, mediaPlayback.duration.inSeconds);
ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); 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(); resetTimer();
final newPosition = (mediaPlayback.position.inSeconds + seconds).clamp(0, mediaPlayback.duration.inSeconds); final newPosition = (mediaPlayback.position.inSeconds + seconds).clamp(0, mediaPlayback.duration.inSeconds);
ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition));
@ -592,6 +597,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
void toggleOverlay({bool? value}) { void toggleOverlay({bool? value}) {
if (showOverlay == (value ?? !showOverlay)) return;
setState(() => showOverlay = (value ?? !showOverlay)); setState(() => showOverlay = (value ?? !showOverlay));
resetTimer(); resetTimer();
SystemChrome.setEnabledSystemUIMode(showOverlay ? SystemUiMode.edgeToEdge : SystemUiMode.leanBack, overlays: []); SystemChrome.setEnabledSystemUIMode(showOverlay ? SystemUiMode.edgeToEdge : SystemUiMode.leanBack, overlays: []);
@ -609,9 +615,17 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
void resetTimer() => timer.reset();
Future<void> closePlayer() async {
clearOverlaySettings();
ref.read(videoPlayerProvider).stop();
Navigator.of(context).pop();
}
Future<void> clearOverlaySettings() async { Future<void> clearOverlaySettings() async {
toggleOverlay(value: true); toggleOverlay(value: true);
if (!AdaptiveLayout.of(context).isDesktop) { if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) {
ScreenBrightness().resetScreenBrightness(); ScreenBrightness().resetScreenBrightness();
} else { } else {
disableFullscreen(); disableFullscreen();
@ -624,19 +638,18 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
timer.cancel(); timer.cancel();
} }
void resetTimer() => timer.reset();
Future<void> closePlayer() async {
clearOverlaySettings();
ref.read(videoPlayerProvider).stop();
Navigator.of(context).pop();
}
Future<void> disableFullscreen() async { Future<void> disableFullscreen() async {
resetTimer(); resetTimer();
final isFullScreen = await windowManager.isFullScreen(); if (kIsWeb) {
if (isFullScreen) { if (html.document.fullscreenElement != null) {
await windowManager.setFullScreen(false); html.document.exitFullscreen();
await Future.delayed(const Duration(milliseconds: 500));
}
} else {
final isFullScreen = await windowManager.isFullScreen();
if (isFullScreen) {
await windowManager.setFullScreen(false);
}
} }
} }

View file

@ -123,7 +123,8 @@ class _NavigationBodyState extends ConsumerState<NavigationBody> {
Flexible( Flexible(
child: Padding( child: Padding(
key: const Key('navigation_rail'), 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( child: Column(
children: [ children: [
IconButton( IconButton(

View file

@ -41,9 +41,8 @@ class NestedNavigationDrawer extends ConsumerWidget {
backgroundColor: isExpanded ? Colors.transparent : null, backgroundColor: isExpanded ? Colors.transparent : null,
surfaceTintColor: isExpanded ? Colors.transparent : null, surfaceTintColor: isExpanded ? Colors.transparent : null,
children: [ children: [
if (AdaptiveLayout.of(context).isDesktop || kIsWeb) const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(28, 16, 16, 0), padding: EdgeInsets.fromLTRB(28, AdaptiveLayout.of(context).isDesktop ? 0 : 16, 16, 0),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(

View file

@ -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<FullScreenButton> createState() => _FullScreenButtonState();
}
class _FullScreenButtonState extends State<FullScreenButton> {
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,
),
);
}
}

View file

@ -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<FullScreenButton> createState() => _FullScreenButtonState();
}
class _FullScreenButtonState extends State<FullScreenButton> {
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,
),
);
}
}