mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-15 18:25:59 -07:00
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:
parent
da9e0423c8
commit
8e2ce7861b
7 changed files with 359 additions and 250 deletions
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
44
lib/widgets/shared/full_screen_button.dart
Normal file
44
lib/widgets/shared/full_screen_button.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/widgets/shared/full_screen_button_web.dart
Normal file
49
lib/widgets/shared/full_screen_button_web.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue