Fix player views

This commit is contained in:
kodjomoustapha 2023-12-29 15:38:00 +01:00
parent a7773f9435
commit 710c498bb1
18 changed files with 2131 additions and 483 deletions

View file

@ -191,6 +191,11 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
'url',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
false),
BridgeParameter(
'prefix',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string),
nullable: true),
true),
]),
),
'myTvExtractor': BridgeMethodDef(
@ -799,7 +804,7 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
}),
"substringBeforeLast" => $Function((_, __, List<$Value?> args) {
return $String(
MBridge.substringBefore(args[0]!.$value, args[1]!.$value));
MBridge.substringBeforeLast(args[0]!.$value, args[1]!.$value));
}),
"substringAfterLast" => $Function((_, __, List<$Value?> args) {
return $String(
@ -808,9 +813,10 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
///////////////////////////////////////////////////////////////////////
"sibnetExtractor" => $Function((_, __, List<$Value?> args) =>
$Future.wrap(MBridge.sibnetExtractor(args[0]!.$value).then(
(value) => $List.wrap(value.map((e) => _toMVideo(e)).toList())))),
"sibnetExtractor" => $Function((_, __, List<$Value?> args) => $Future
.wrap(MBridge.sibnetExtractor(args[0]!.$value, args[1]?.$value ?? "")
.then((value) =>
$List.wrap(value.map((e) => _toMVideo(e)).toList())))),
"myTvExtractor" => $Function((_, __, List<$Value?> args) => $Future.wrap(
MBridge.myTvExtractor(args[0]!.$value).then(
(value) => $List.wrap(value.map((e) => _toMVideo(e)).toList())))),

View file

@ -646,8 +646,8 @@ class MBridge {
return Deobfuscator.deobfuscateJsPassword(inputString);
}
static Future<List<Video>> sibnetExtractor(String url) async {
return await SibnetExtractor().videosFromUrl(url);
static Future<List<Video>> sibnetExtractor(String url, String prefix) async {
return await SibnetExtractor().videosFromUrl(url, prefix: prefix);
}
static Future<List<Video>> sendVidExtractor(

View file

@ -18,6 +18,7 @@ import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:media_kit/media_kit.dart';
import 'package:rinf/rinf.dart';
import 'package:window_manager/window_manager.dart';
// Global instance of the Isar database.
late Isar isar;
@ -46,7 +47,7 @@ void main(List<String> args) async {
// Ensure widget and media kits are initialized.
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
await windowManager.ensureInitialized();
// Initialize the Isar database.
isar = await StorageProvider().initDB(null);
await StorageProvider().requestPermission();

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:draggable_menu/draggable_menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -8,19 +9,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/video.dart' as vid;
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart';
import 'package:mangayomi/modules/anime/widgets/indicator_builder.dart';
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/anime/widgets/mobile.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/get_video_list.dart';
import 'package:mangayomi/utils/colors.dart';
import 'package:mangayomi/utils/media_query.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:window_manager/window_manager.dart';
class AnimePlayerView extends riv.ConsumerStatefulWidget {
final Chapter episode;
@ -211,7 +211,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_currentTotalDuration.value = duration;
},
);
double _brightnessValue = 0.0;
@override
void initState() {
_setCurrentPosition(true);
@ -220,18 +220,6 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_player.open(Media(_video.value!.videoTrack!.id,
httpHeaders: _video.value!.headers));
_setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider));
Future.microtask(() async {
try {
_brightnessValue = await ScreenBrightness().current;
ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
if (mounted) {
setState(() {
_brightnessValue = value;
});
}
});
} catch (_) {}
});
super.initState();
}
@ -241,7 +229,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_player.dispose();
_currentPositionSub.cancel();
_currentTotalDurationSub.cancel();
_setFullscreen(false);
_setLandscapeMode(false);
super.dispose();
}
@ -252,7 +240,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_streamController.setAnimeHistoryUpdate();
}
void _setFullscreen(bool state) {
void _setLandscapeMode(bool state) {
if (state) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
@ -685,8 +673,11 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
}
_fit.value = fit;
_key.currentState?.update(fit: fit);
await Future.delayed(const Duration(seconds: 2));
_showFitLabel.value = false;
BotToast.showText(
onlyOne: true,
align: const Alignment(0, 0.90),
duration: const Duration(seconds: 1),
text: fit.name.toUpperCase());
}
Widget _seekToWidget() {
@ -716,168 +707,28 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
);
}
List<Widget> _mobileBottomButtonBar(BuildContext context, bool isFullScreen) {
return [
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_seekToWidget(),
Row(
children: [
if (!isFullScreen)
IconButton(
padding: const EdgeInsets.all(5),
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(
Icons.video_settings,
color: Colors.white,
),
),
TextButton(
child: ValueListenableBuilder<double>(
valueListenable: _playbackSpeed,
builder: (context, value, child) {
return Text(
"${value}x",
style: const TextStyle(color: Colors.white),
);
},
),
onPressed: () {
_togglePlaybackSpeed();
}),
IconButton(
icon: const Icon(Icons.fit_screen_outlined,
color: Colors.white),
onPressed: () async {
_changeFitLabel(ref);
},
),
ValueListenableBuilder<bool>(
valueListenable: _enterFullScreen,
builder: (context, snapshot, _) {
return IconButton(
onPressed: () {
_setFullscreen(!snapshot);
_enterFullScreen.value = !snapshot;
},
icon: Icon(snapshot
? Icons.fullscreen_exit
: Icons.fullscreen),
iconSize: 25,
color: Colors.white,
);
})
],
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: CustomSeekBar(
player: _controller.player,
onSeekStart: (start) {
_tempPosition.value = start;
},
onSeekEnd: (end) {
_tempPosition.value = null;
},
),
),
],
),
)
];
}
List<Widget> _desktopBottomButtonBar(
BuildContext context, bool isFullScreen) {
bool hasPrevEpisode = _streamController.getEpisodeIndex().$1 + 1 !=
_streamController
.getEpisodesLength(_streamController.getEpisodeIndex().$2);
bool hasNextEpisode = _streamController.getEpisodeIndex().$1 != 0;
return [
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
children: [
_seekToWidget(),
],
),
),
SizedBox(
height: 20,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: CustomSeekBar(
player: _controller.player,
onSeekStart: (start) {
_tempPosition.value = start;
},
onSeekEnd: (end) {
_tempPosition.value = null;
},
),
)),
Row(
Widget _mobileBottomButtonBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_seekToWidget(),
Row(
children: [
if (hasPrevEpisode)
IconButton(
onPressed: () {
if (isFullScreen) {
_key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getPrevEpisode());
},
icon: const Icon(
Icons.skip_previous,
color: Colors.white,
),
),
const MaterialDesktopPlayOrPauseButton(iconSize: 25),
if (hasNextEpisode)
IconButton(
onPressed: () {
if (isFullScreen) {
_key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getNextEpisode(),
);
},
icon: const Icon(Icons.skip_next, color: Colors.white),
),
const MaterialDesktopVolumeButton(iconSize: 25),
const MaterialDesktopPositionIndicator()
],
),
Row(
children: [
if (!isFullScreen)
IconButton(
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(
Icons.video_settings,
color: Colors.white,
),
IconButton(
padding: const EdgeInsets.all(5),
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(
Icons.video_settings,
color: Colors.white,
),
),
TextButton(
child: ValueListenableBuilder<double>(
valueListenable: _playbackSpeed,
@ -898,73 +749,187 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_changeFitLabel(ref);
},
),
const MaterialDesktopFullscreenButton()
ValueListenableBuilder<bool>(
valueListenable: _enterFullScreen,
builder: (context, snapshot, _) {
return IconButton(
onPressed: () {
_setLandscapeMode(!snapshot);
_enterFullScreen.value = !snapshot;
},
icon: Icon(snapshot
? Icons.fullscreen_exit
: Icons.fullscreen),
iconSize: 25,
color: Colors.white,
);
})
],
),
],
),
],
),
)
];
),
],
),
);
}
List<Widget> _topButtonBar(BuildContext context, bool isFullScreen) {
return [
Flexible(
child: Row(
Widget _desktopBottomButtonBar(BuildContext context) {
bool hasPrevEpisode = _streamController.getEpisodeIndex().$1 + 1 !=
_streamController
.getEpisodesLength(_streamController.getEpisodeIndex().$2);
bool hasNextEpisode = _streamController.getEpisodeIndex().$1 != 0;
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isFullScreen && (Platform.isIOS || Platform.isAndroid)) ...[
MaterialFullscreenButton(
icon: Icon(Platform.isIOS || Platform.isMacOS
? Icons.arrow_back_ios
: Icons.arrow_back),
)
] else ...[
if (isFullScreen)
MaterialDesktopFullscreenButton(
icon: Icon(Platform.isMacOS
? Icons.arrow_back_ios
: Icons.arrow_back))
],
if (!isFullScreen)
BackButton(
color: Colors.white,
onPressed: () {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
Navigator.pop(context);
},
),
Flexible(
child: ListTile(
dense: true,
title: SizedBox(
width: mediaWidth(context, 0.8),
child: Text(
widget.episode.manga.value!.name!,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white),
overflow: TextOverflow.ellipsis,
Row(
children: [
if (hasPrevEpisode)
IconButton(
onPressed: () async {
if (_isDesktop) {
final isFullScreen = await windowManager.isFullScreen();
if (isFullScreen) {
await setFullScreen(value: false);
}
}
if (mounted) {
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getPrevEpisode());
}
},
icon: const Icon(
Icons.skip_previous,
color: Colors.white,
),
),
CustomeMaterialDesktopPlayOrPauseButton(
controller: _controller,
),
if (hasNextEpisode)
IconButton(
onPressed: () async {
if (_isDesktop) {
final isFullScreen = await windowManager.isFullScreen();
if (isFullScreen) {
await setFullScreen(value: false);
}
}
if (mounted) {
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getNextEpisode(),
);
}
},
icon: const Icon(Icons.skip_next, color: Colors.white),
),
CustomMaterialDesktopVolumeButton(
controller: _controller,
),
ValueListenableBuilder(
valueListenable: _tempPosition,
builder: (context, value, child) =>
CustomMaterialDesktopPositionIndicator(
delta: value, controller: _controller),
)
],
),
Row(
children: [
IconButton(
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(
Icons.video_settings,
color: Colors.white,
),
),
subtitle: SizedBox(
width: mediaWidth(context, 0.8),
child: Text(
widget.episode.name!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white.withOpacity(0.7)),
overflow: TextOverflow.ellipsis,
),
TextButton(
child: ValueListenableBuilder<double>(
valueListenable: _playbackSpeed,
builder: (context, value, child) {
return Text(
"${value}x",
style: const TextStyle(color: Colors.white),
);
},
),
onPressed: () {
_togglePlaybackSpeed();
}),
IconButton(
icon: const Icon(Icons.fit_screen_outlined,
color: Colors.white),
onPressed: () async {
_changeFitLabel(ref);
},
),
),
CustomMaterialDesktopFullscreenButton(
controller: _controller,
)
],
),
],
),
),
];
],
);
}
Widget _topButtonBar(BuildContext context) {
return Row(
children: [
BackButton(
color: Colors.white,
onPressed: () async {
if (_isDesktop) {
final isFullScreen = await windowManager.isFullScreen();
if (isFullScreen) {
setFullScreen(value: false);
} else {
if (mounted) {
Navigator.pop(context);
}
}
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
if (mounted) {
Navigator.pop(context);
}
}
},
),
Flexible(
child: ListTile(
dense: true,
title: SizedBox(
width: mediaWidth(context, 0.8),
child: Text(
widget.episode.manga.value!.name!,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white),
overflow: TextOverflow.ellipsis,
),
),
subtitle: SizedBox(
width: mediaWidth(context, 0.8),
child: Text(
widget.episode.name!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white.withOpacity(0.7)),
overflow: TextOverflow.ellipsis,
),
),
),
),
],
);
}
void _resize(BoxFit fit) async {
@ -980,173 +945,51 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
Widget _videoPlayer(BuildContext context) {
final fit = _fit.value;
_resize(fit);
return Stack(
children: [
Video(
subtitleViewConfiguration: const SubtitleViewConfiguration(
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: "",
shadows: [Shadow(offset: Offset(0.2, 0.0), blurRadius: 7.0)],
backgroundColor: Colors.transparent),
),
fit: fit,
key: _key,
controller: _controller,
width: mediaWidth(context, 1),
height: mediaHeight(context, 1),
resumeUponEnteringForegroundMode: true,
),
ValueListenableBuilder(
valueListenable: _showFitLabel,
builder: (context, showFitLabel, child) => showFitLabel
? ValueListenableBuilder(
valueListenable: _fit,
builder: (context, fit, child) => Positioned.fill(
child: Positioned.fill(
child: Center(
child: Text(
fit.name.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 40.0),
))),
),
)
: Container()),
],
return Video(
subtitleViewConfiguration: const SubtitleViewConfiguration(
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
color: Colors.white,
fontFamily: "",
shadows: [Shadow(offset: Offset(0.2, 0.0), blurRadius: 7.0)],
backgroundColor: Colors.transparent),
),
fit: fit,
key: _key,
controls: (state) => _isDesktop
? DestopControllerWidget(
videoController: _controller,
topButtonBarWidget: _topButtonBar(context),
videoStatekey: _key,
bottomButtonBarWidget: _desktopBottomButtonBar(context),
streamController: _streamController,
seekToWidget: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
children: [
_seekToWidget(),
],
),
),
tempDuration: (value) {
_tempPosition.value = value;
},
)
: MobileControllerWidget(
videoController: _controller,
topButtonBarWidget: _topButtonBar(context),
videoStatekey: _key,
bottomButtonBarWidget: _mobileBottomButtonBar(context),
streamController: _streamController,
),
controller: _controller,
width: mediaWidth(context, 1),
height: mediaHeight(context, 1),
resumeUponEnteringForegroundMode: true,
);
}
Widget _mobilePlayer() {
MaterialVideoControlsThemeData materialVideoControlsThemeData(
bool isFullScreen) =>
MaterialVideoControlsThemeData(
visibleOnMount: true,
buttonBarHeight: 100,
seekOnDoubleTap: true,
seekGesture: true,
horizontalGestureSensitivity: 5000,
verticalGestureSensitivity: 500,
controlsHoverDuration: const Duration(seconds: 15),
volumeGesture: true,
brightnessGesture: true,
seekBarThumbSize: 15,
seekBarHeight: 5,
displaySeekBar: false,
volumeIndicatorBuilder: (_, value) =>
MediaIndicatorBuilder(value: value, isVolumeIndicator: true),
brightnessIndicatorBuilder: (_, value) => MediaIndicatorBuilder(
value: _brightnessValue, isVolumeIndicator: false),
seekIndicatorBuilder: (context, duration) {
return _seekIndicatorTextWidget(duration, _currentPosition.value);
},
seekBarPositionColor: primaryColor(context),
seekBarThumbColor: primaryColor(context),
primaryButtonBar: [
ValueListenableBuilder<Duration?>(
valueListenable: _tempPosition,
builder: (context, snapshot, _) {
return snapshot != null
? _seekIndicatorTextWidget(
snapshot, _currentPosition.value)
: Expanded(
child: Row(
children: _mobilePrimaryButtonBar(isFullScreen),
),
);
})
],
topButtonBarMargin: const EdgeInsets.all(0),
topButtonBar: _topButtonBar(context, isFullScreen),
bottomButtonBarMargin: const EdgeInsets.only(left: 8, right: 8),
bottomButtonBar: _mobileBottomButtonBar(context, isFullScreen));
return MaterialVideoControlsTheme(
normal: materialVideoControlsThemeData(false),
fullscreen: materialVideoControlsThemeData(true),
child: _videoPlayer(context));
}
List<Widget> _mobilePrimaryButtonBar(bool isFullScreen) {
bool hasPrevEpisode = _streamController.getEpisodeIndex().$1 + 1 !=
_streamController
.getEpisodesLength(_streamController.getEpisodeIndex().$2);
bool hasNextEpisode = _streamController.getEpisodeIndex().$1 != 0;
return [
const Spacer(flex: 3),
IconButton(
onPressed: hasPrevEpisode
? () {
if (isFullScreen) {
_key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getPrevEpisode());
}
: null,
icon: Icon(
Icons.skip_previous,
size: 35,
color: hasPrevEpisode ? Colors.white : Colors.grey,
),
),
const Spacer(),
const MaterialPlayOrPauseButton(iconSize: 65),
const Spacer(),
IconButton(
onPressed: hasNextEpisode
? () {
if (isFullScreen) {
_key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context,
chapter: _streamController.getNextEpisode(),
);
}
: null,
icon: Icon(Icons.skip_next,
size: 35, color: hasPrevEpisode ? Colors.white : Colors.grey),
),
const Spacer(flex: 3)
];
}
Widget _desktopPlayer() {
MaterialDesktopVideoControlsThemeData materialVideoControlsThemeData(
bool isFullScreen) =>
MaterialDesktopVideoControlsThemeData(
visibleOnMount: true,
controlsHoverDuration: const Duration(seconds: 2),
seekBarPositionColor: primaryColor(context),
seekBarThumbColor: primaryColor(context),
topButtonBarMargin: const EdgeInsets.all(0),
bottomButtonBarMargin: const EdgeInsets.all(0),
topButtonBar: _topButtonBar(context, isFullScreen),
primaryButtonBar: [
ValueListenableBuilder<Duration?>(
valueListenable: _tempPosition,
builder: (context, snapshot, _) {
return snapshot != null
? _seekIndicatorTextWidget(
snapshot, _currentPosition.value)
: const SizedBox.shrink();
})
],
buttonBarHeight: 120,
displaySeekBar: false,
seekBarThumbSize: 15,
bottomButtonBar: _desktopBottomButtonBar(context, isFullScreen));
return MaterialDesktopVideoControlsTheme(
normal: materialVideoControlsThemeData(false),
fullscreen: materialVideoControlsThemeData(true),
child: _videoPlayer(context));
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -1157,13 +1000,13 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
Navigator.pop(context);
return false;
},
child: _isDesktop ? _desktopPlayer() : _mobilePlayer(),
child: _videoPlayer(context),
),
);
}
}
Widget _seekIndicatorTextWidget(Duration duration, Duration currentPosition) {
Widget seekIndicatorTextWidget(Duration duration, Duration currentPosition) {
final swipeDuration = duration.inSeconds;
final value = currentPosition.inSeconds + swipeDuration;
return Column(

View file

@ -168,4 +168,5 @@ class AnimeStreamController extends _$AnimeStreamController {
}
}
}
}

View file

@ -7,14 +7,16 @@ import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions
class CustomSeekBar extends StatefulWidget {
final Player player;
final Function(Duration) onSeekStart;
final Function(Duration) onSeekEnd;
final Duration? delta;
final Function(Duration)? onSeekStart;
final Function(Duration)? onSeekEnd;
const CustomSeekBar(
{super.key,
required this.onSeekStart,
required this.onSeekEnd,
required this.player});
this.onSeekStart,
this.onSeekEnd,
required this.player,
this.delta});
@override
CustomSeekBarState createState() => CustomSeekBarState();
@ -68,9 +70,8 @@ class CustomSeekBarState extends State<CustomSeekBar> {
width: 70,
child: Center(
child: Text(
tempPosition != null
? tempPosition!.label(reference: duration)
: position.label(reference: duration),
(widget.delta ?? tempPosition ?? position)
.label(reference: duration),
style: const TextStyle(
height: 1.0,
fontSize: 12.0,
@ -86,10 +87,13 @@ class CustomSeekBarState extends State<CustomSeekBar> {
child: Slider(
max: max(duration.inMilliseconds.toDouble(), 0),
value: max(
(tempPosition ?? position).inMilliseconds.toDouble(), 0),
(widget.delta ?? tempPosition ?? position)
.inMilliseconds
.toDouble(),
0),
secondaryTrackValue: max(buffer.inMilliseconds.toDouble(), 0),
onChanged: (value) {
widget.onSeekStart(Duration(
widget.onSeekStart?.call(Duration(
milliseconds: value.toInt() - position.inMilliseconds));
if (mounted) {
setState(() {
@ -98,7 +102,7 @@ class CustomSeekBarState extends State<CustomSeekBar> {
}
},
onChangeEnd: (value) async {
widget.onSeekEnd(Duration(
widget.onSeekEnd?.call(Duration(
milliseconds: value.toInt() - position.inMilliseconds));
widget.player.seek(Duration(milliseconds: value.toInt()));
await Future.delayed(const Duration(milliseconds: 500));

View file

@ -0,0 +1,821 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mangayomi/modules/anime/anime_player_view.dart';
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:window_manager/window_manager.dart';
class DestopControllerWidget extends StatefulWidget {
final Function(Duration?) tempDuration;
final AnimeStreamController streamController;
final VideoController videoController;
final Widget topButtonBarWidget;
final GlobalKey<VideoState> videoStatekey;
final Widget bottomButtonBarWidget;
final Widget seekToWidget;
const DestopControllerWidget(
{super.key,
required this.videoController,
required this.topButtonBarWidget,
required this.bottomButtonBarWidget,
required this.streamController,
required this.videoStatekey,
required this.seekToWidget,
required this.tempDuration});
@override
State<DestopControllerWidget> createState() => _DestopControllerWidgetState();
}
class _DestopControllerWidgetState extends State<DestopControllerWidget> {
bool mount = true;
bool visible = true;
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
Color backdropColor = const Color(0x66000000);
Timer? _timer;
int swipeDuration = 0; // Duration to seek in video
bool showSwipeDuration = false; // Whether to show the seek duration overlay
late bool buffering = widget.videoController.player.state.buffering;
final controlsHoverDuration = const Duration(seconds: 3);
double buttonBarHeight = 100;
final bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0);
final List<StreamSubscription> subscriptions = [];
DateTime last = DateTime.now();
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (subscriptions.isEmpty) {
subscriptions.addAll(
[
widget.videoController.player.stream.buffering.listen(
(event) {
setState(() {
buffering = event;
});
},
),
],
);
_timer = Timer(
controlsHoverDuration,
() {
if (mounted) {
setState(() {
visible = false;
});
}
},
);
}
}
@override
void dispose() {
for (final subscription in subscriptions) {
subscription.cancel();
}
super.dispose();
}
void onHover() {
setState(() {
mount = true;
visible = true;
});
_timer?.cancel();
_timer = Timer(controlsHoverDuration, () {
if (mounted) {
setState(() {
visible = false;
});
}
});
}
void onEnter() {
setState(() {
mount = true;
visible = true;
});
_timer?.cancel();
_timer = Timer(controlsHoverDuration, () {
if (mounted) {
setState(() {
visible = false;
});
}
});
}
void onExit() {
setState(() {
visible = false;
});
_timer?.cancel();
}
final bool modifyVolumeOnScroll = true;
final bool toggleFullscreenOnDoublePress = true;
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
// Default key-board shortcuts.
// https://support.google.com/youtube/answer/7631406
const SingleActivator(LogicalKeyboardKey.mediaPlay): () =>
widget.videoController.player.play(),
const SingleActivator(LogicalKeyboardKey.mediaPause): () =>
widget.videoController.player.pause(),
const SingleActivator(LogicalKeyboardKey.mediaPlayPause): () =>
widget.videoController.player.playOrPause(),
const SingleActivator(LogicalKeyboardKey.mediaTrackNext): () =>
widget.videoController.player.next(),
const SingleActivator(LogicalKeyboardKey.mediaTrackPrevious): () =>
widget.videoController.player.previous(),
const SingleActivator(LogicalKeyboardKey.space): () =>
widget.videoController.player.playOrPause(),
const SingleActivator(LogicalKeyboardKey.keyJ): () {
final rate = widget.videoController.player.state.position -
const Duration(seconds: 10);
widget.videoController.player.seek(rate);
},
const SingleActivator(LogicalKeyboardKey.keyI): () {
final rate = widget.videoController.player.state.position +
const Duration(seconds: 10);
widget.videoController.player.seek(rate);
},
const SingleActivator(LogicalKeyboardKey.arrowLeft): () {
final rate = widget.videoController.player.state.position -
const Duration(seconds: 2);
widget.videoController.player.seek(rate);
},
const SingleActivator(LogicalKeyboardKey.arrowRight): () {
final rate = widget.videoController.player.state.position +
const Duration(seconds: 2);
widget.videoController.player.seek(rate);
},
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
final volume = widget.videoController.player.state.volume + 5.0;
widget.videoController.player.setVolume(volume.clamp(0.0, 100.0));
},
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
final volume = widget.videoController.player.state.volume - 5.0;
widget.videoController.player.setVolume(volume.clamp(0.0, 100.0));
},
const SingleActivator(LogicalKeyboardKey.keyF): () => setFullScreen(),
const SingleActivator(LogicalKeyboardKey.escape): () =>
setFullScreen(value: false),
},
child: Focus(
autofocus: true,
child: Listener(
onPointerSignal: modifyVolumeOnScroll
? (e) {
if (e is PointerScrollEvent) {
if (e.delta.dy > 0) {
final volume =
widget.videoController.player.state.volume - 5.0;
widget.videoController.player
.setVolume(volume.clamp(0.0, 100.0));
}
if (e.delta.dy < 0) {
final volume =
widget.videoController.player.state.volume + 5.0;
widget.videoController.player
.setVolume(volume.clamp(0.0, 100.0));
}
}
}
: null,
child: GestureDetector(
onTapUp: !toggleFullscreenOnDoublePress
? null
: (e) {
final now = DateTime.now();
final difference = now.difference(last);
last = now;
if (difference < const Duration(milliseconds: 400)) {
setFullScreen();
}
},
onPanUpdate: modifyVolumeOnScroll
? (e) {
if (e.delta.dy > 0) {
final volume =
widget.videoController.player.state.volume - 5.0;
widget.videoController.player
.setVolume(volume.clamp(0.0, 100.0));
}
if (e.delta.dy < 0) {
final volume =
widget.videoController.player.state.volume + 5.0;
widget.videoController.player
.setVolume(volume.clamp(0.0, 100.0));
}
}
: null,
child: MouseRegion(
onHover: (_) => onHover(),
onEnter: (_) => onEnter(),
onExit: (_) => onExit(),
child: Stack(
children: [
AnimatedOpacity(
curve: Curves.easeInOut,
opacity: visible ? 1.0 : 0.0,
duration: controlsTransitionDuration,
onEnd: () {
if (!visible) {
setState(() {
mount = false;
});
}
},
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter,
children: [
// Top gradient.
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [
0.0,
0.2,
],
colors: [
Color(0x61000000),
Color(0x00000000),
],
),
),
),
// Bottom gradient.
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [
0.5,
1.0,
],
colors: [
Color(0x00000000),
Color(0x61000000),
],
),
),
),
if (mount)
Padding(
padding: (
// Add padding in fullscreen!
isFullscreen(context)
? MediaQuery.of(context).padding
: EdgeInsets.zero),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
widget.topButtonBarWidget,
// Only display [primaryButtonBar] if [buffering] is false.
Expanded(
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: buffering
? 0.0
: !showSwipeDuration
? 0.0
: 1.0,
duration: controlsTransitionDuration,
child: Center(
child: seekIndicatorTextWidget(
Duration(seconds: swipeDuration),
widget.videoController.player
.state.position))),
),
widget.seekToWidget,
Transform.translate(
offset: Offset.zero,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5),
child: CustomSeekBar(
onSeekStart: (value) {
setState(() {
swipeDuration = value.inSeconds;
showSwipeDuration = true;
widget.tempDuration(widget
.videoController
.player
.state
.position +
value);
});
_timer?.cancel();
},
onSeekEnd: (value) {
_timer = Timer(
controlsHoverDuration,
() {
if (mounted) {
setState(() {
visible = false;
});
}
},
);
setState(() {
showSwipeDuration = false;
});
widget.tempDuration(null);
},
player: widget.videoController.player,
),
),
),
widget.bottomButtonBarWidget
],
),
),
],
),
),
// Buffering Indicator.
IgnorePointer(
child: Padding(
padding: (
// Add padding in fullscreen!
isFullscreen(context)
? MediaQuery.of(context).padding
: EdgeInsets.zero),
child: Column(
children: [
Container(
height: buttonBarHeight,
margin: const EdgeInsets.all(0),
),
Expanded(
child: Center(
child: Center(
child: TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: buffering ? 1.0 : 0.0,
),
duration: controlsTransitionDuration,
builder: (context, value, child) {
// Only mount the buffering indicator if the opacity is greater than 0.0.
// This has been done to prevent redundant resource usage in [CircularProgressIndicator].
if (value > 0.0) {
return Opacity(
opacity: value,
child: child!,
);
}
return const SizedBox.shrink();
},
child: const CircularProgressIndicator(
color: Color(0xFFFFFFFF),
),
),
),
),
),
Container(
height: buttonBarHeight,
margin: bottomButtonBarMargin,
),
],
),
),
),
],
),
),
),
),
),
);
}
}
// BUTTON: PLAY/PAUSE
/// A material design play/pause button.
class CustomeMaterialDesktopPlayOrPauseButton extends StatefulWidget {
final VideoController controller;
const CustomeMaterialDesktopPlayOrPauseButton({
super.key,
required this.controller,
});
@override
CustomeMaterialDesktopPlayOrPauseButtonState createState() =>
CustomeMaterialDesktopPlayOrPauseButtonState();
}
class CustomeMaterialDesktopPlayOrPauseButtonState
extends State<CustomeMaterialDesktopPlayOrPauseButton>
with SingleTickerProviderStateMixin {
late final animation = AnimationController(
vsync: this,
value: widget.controller.player.state.playing ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
StreamSubscription<bool>? subscription;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
subscription ??= widget.controller.player.stream.playing.listen((event) {
if (event) {
animation.forward();
} else {
animation.reverse();
}
});
}
@override
void dispose() {
animation.dispose();
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: widget.controller.player.playOrPause,
iconSize: 25,
color: Colors.white,
icon: AnimatedIcon(
progress: animation,
icon: AnimatedIcons.play_pause,
size: 25,
color: Colors.white,
),
);
}
}
// BUTTON: VOLUME
/// MaterialDesktop design volume button & slider.
class CustomMaterialDesktopVolumeButton extends StatefulWidget {
final VideoController controller;
const CustomMaterialDesktopVolumeButton({
super.key,
required this.controller,
});
@override
CustomMaterialDesktopVolumeButtonState createState() =>
CustomMaterialDesktopVolumeButtonState();
}
class CustomMaterialDesktopVolumeButtonState
extends State<CustomMaterialDesktopVolumeButton>
with SingleTickerProviderStateMixin {
late double volume = widget.controller.player.state.volume;
StreamSubscription<double>? subscription;
bool hover = false;
bool mute = false;
double _volume = 0.0;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
subscription ??= widget.controller.player.stream.volume.listen((event) {
setState(() {
volume = event;
});
});
}
@override
void dispose() {
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (e) {
setState(() {
hover = true;
});
},
onExit: (e) {
setState(() {
hover = false;
});
},
child: Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
if (event.scrollDelta.dy < 0) {
widget.controller.player.setVolume(
(volume + 5.0).clamp(0.0, 100.0),
);
}
if (event.scrollDelta.dy > 0) {
widget.controller.player.setVolume(
(volume - 5.0).clamp(0.0, 100.0),
);
}
}
},
child: Row(
children: [
const SizedBox(width: 4.0),
IconButton(
onPressed: () async {
if (mute) {
await widget.controller.player.setVolume(_volume);
mute = !mute;
}
// https://github.com/media-kit/media-kit/pull/250#issuecomment-1605588306
else if (volume == 0.0) {
_volume = 100.0;
await widget.controller.player.setVolume(100.0);
mute = false;
} else {
_volume = volume;
await widget.controller.player.setVolume(0.0);
mute = !mute;
}
setState(() {});
},
iconSize: 25,
color: Colors.white,
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
child: volume == 0.0
? const Icon(
Icons.volume_off,
key: ValueKey(Icons.volume_off),
)
: volume < 50.0
? const Icon(
Icons.volume_down,
key: ValueKey(Icons.volume_down),
)
: const Icon(
Icons.volume_up,
key: ValueKey(Icons.volume_up),
),
),
),
AnimatedOpacity(
opacity: hover ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: AnimatedContainer(
width: hover ? (12.0 + 52.0 + 18.0) : 12.0,
duration: const Duration(milliseconds: 150),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
const SizedBox(width: 12.0),
SizedBox(
width: 52.0,
child: SliderTheme(
data: SliderThemeData(
trackHeight: 1.2,
inactiveTrackColor: const Color(0x3DFFFFFF),
activeTrackColor: Colors.white,
thumbColor: Colors.white,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 12 / 2,
elevation: 0.0,
pressedElevation: 0.0,
),
trackShape: _CustomTrackShape(),
overlayColor: const Color(0x00000000),
),
child: Slider(
value: volume.clamp(0.0, 100.0),
min: 0.0,
max: 100.0,
onChanged: (value) async {
await widget.controller.player.setVolume(value);
mute = false;
setState(() {});
},
),
),
),
const SizedBox(width: 18.0),
],
),
),
),
),
],
),
),
);
}
}
// POSITION INDICATOR
/// MaterialDesktop design position indicator.
class CustomMaterialDesktopPositionIndicator extends StatefulWidget {
final VideoController controller;
final Duration? delta;
const CustomMaterialDesktopPositionIndicator(
{super.key, required this.controller, this.delta});
@override
CustomMaterialDesktopPositionIndicatorState createState() =>
CustomMaterialDesktopPositionIndicatorState();
}
class CustomMaterialDesktopPositionIndicatorState
extends State<CustomMaterialDesktopPositionIndicator> {
late Duration position = widget.controller.player.state.position;
late Duration duration = widget.controller.player.state.duration;
final List<StreamSubscription> subscriptions = [];
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (subscriptions.isEmpty) {
subscriptions.addAll(
[
widget.controller.player.stream.position.listen((event) {
setState(() {
position = event;
});
}),
widget.controller.player.stream.duration.listen((event) {
setState(() {
duration = event;
});
}),
],
);
}
}
@override
void dispose() {
for (final subscription in subscriptions) {
subscription.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
'${(widget.delta ?? position).label(reference: duration)} / ${duration.label(reference: duration)}',
style: const TextStyle(
height: 1.0,
fontSize: 12.0,
color: Colors.white,
),
);
}
}
class _CustomTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final height = sliderTheme.trackHeight;
final left = offset.dx;
final top = offset.dy + (parentBox.size.height - height!) / 2;
final width = parentBox.size.width;
return Rect.fromLTWH(
left,
top,
width,
height,
);
}
}
class CustomMaterialDesktopFullscreenButton extends StatefulWidget {
final VideoController controller;
const CustomMaterialDesktopFullscreenButton({
super.key,
required this.controller,
});
@override
State<CustomMaterialDesktopFullscreenButton> createState() =>
_CustomMaterialDesktopFullscreenButtonState();
}
class _CustomMaterialDesktopFullscreenButtonState
extends State<CustomMaterialDesktopFullscreenButton> {
bool _isFullscreen = false;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () async {
final isFullScreen = await setFullScreen();
setState(() {
_isFullscreen = isFullScreen;
});
},
icon: _isFullscreen
? const Icon(Icons.fullscreen_exit)
: const Icon(Icons.fullscreen),
iconSize: 25,
color: Colors.white,
);
}
}
Future<bool> setFullScreen({bool? value}) async {
if (value != null) {
await windowManager.setTitleBarStyle(
value == false ? TitleBarStyle.normal : TitleBarStyle.hidden);
await windowManager.setFullScreen(value);
if (value == false) {
await windowManager.center();
}
await windowManager.show();
return value;
}
final isFullScreen = await windowManager.isFullScreen();
if (!isFullScreen) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
await windowManager.setFullScreen(true);
await windowManager.show();
} else {
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
await windowManager.setFullScreen(false);
await windowManager.center();
await windowManager.show();
}
return isFullScreen;
}

View file

@ -2,73 +2,76 @@ import 'package:flutter/material.dart';
class MediaIndicatorBuilder extends StatelessWidget {
final bool isVolumeIndicator;
final double value;
final ValueNotifier<double> value;
const MediaIndicatorBuilder(
{super.key, required this.value, required this.isVolumeIndicator});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Row(
mainAxisAlignment:
isVolumeIndicator ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(100),
),
width: 30,
child: UnconstrainedBox(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Text(
(value * 100).ceil().toString(),
style: const TextStyle(color: Colors.white),
return ValueListenableBuilder(
valueListenable: value,
builder: (context, value, child) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Row(
mainAxisAlignment: isVolumeIndicator
? MainAxisAlignment.start
: MainAxisAlignment.end,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(100),
),
Padding(
padding: const EdgeInsets.all(5),
child: RotatedBox(
quarterTurns: -1,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(100),
),
child: SizedBox.fromSize(
size: const Size(130, 20),
child: LinearProgressIndicator(
value: value,
backgroundColor: Colors.transparent)),
width: 30,
child: UnconstrainedBox(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Text(
(value * 100).ceil().toString(),
style: const TextStyle(color: Colors.white),
),
Padding(
padding: const EdgeInsets.all(5),
child: RotatedBox(
quarterTurns: -1,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(100),
),
child: SizedBox.fromSize(
size: const Size(130, 20),
child: LinearProgressIndicator(
value: value,
backgroundColor: Colors.transparent)),
),
),
),
Icon(
isVolumeIndicator
? switch (value) {
== 0.0 => Icons.volume_off,
< 0.5 => Icons.volume_down,
_ => Icons.volume_up,
}
: switch (value) {
< 1.0 / 3.0 => Icons.brightness_low,
< 2.0 / 3.0 => Icons.brightness_medium,
_ => Icons.brightness_high,
},
color: Colors.white,
)
],
),
),
),
Icon(
isVolumeIndicator
? switch (value) {
== 0.0 => Icons.volume_off,
< 0.5 => Icons.volume_down,
_ => Icons.volume_up,
}
: switch (value) {
< 1.0 / 3.0 => Icons.brightness_low,
< 2.0 / 3.0 => Icons.brightness_medium,
_ => Icons.brightness_high,
},
color: Colors.white,
)
],
),
),
],
),
),
),
],
),
);
));
}
}

View file

@ -0,0 +1,933 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'package:mangayomi/modules/anime/anime_player_view.dart';
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart';
import 'package:mangayomi/modules/anime/widgets/indicator_builder.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:volume_controller/volume_controller.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:flutter/material.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
class MobileControllerWidget extends StatefulWidget {
final AnimeStreamController streamController;
final VideoController videoController;
final Widget topButtonBarWidget;
final GlobalKey<VideoState> videoStatekey;
final Widget bottomButtonBarWidget;
const MobileControllerWidget(
{super.key,
required this.videoController,
required this.topButtonBarWidget,
required this.bottomButtonBarWidget,
required this.streamController,
required this.videoStatekey});
@override
State<MobileControllerWidget> createState() => _MobileControllerWidgetState();
}
class _MobileControllerWidgetState extends State<MobileControllerWidget> {
bool mount = true;
bool visible = true;
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
Color backdropColor = const Color(0x66000000);
Timer? _timer;
final ValueNotifier<double> _brightnessValue = ValueNotifier(0.0);
final ValueNotifier<bool> _brightnessIndicator = ValueNotifier(false);
Timer? _brightnessTimer;
final ValueNotifier<double> _volumeValue = ValueNotifier(0.0);
final ValueNotifier<bool> _volumeIndicator = ValueNotifier(false);
Timer? _volumeTimer;
// The default event stream in package:volume_controller is buggy.
bool _volumeInterceptEventStream = false;
Offset _dragInitialDelta =
Offset.zero; // Initial position for horizontal drag
int swipeDuration = 0; // Duration to seek in video
bool showSwipeDuration = false; // Whether to show the seek duration overlay
late bool buffering = widget.videoController.player.state.buffering;
final controlsHoverDuration = const Duration(seconds: 3);
bool _mountSeekBackwardButton = false;
bool _mountSeekForwardButton = false;
bool _hideSeekBackwardButton = false;
bool _hideSeekForwardButton = false;
double buttonBarHeight = 100;
final bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0);
Duration? _seekBarDeltaValueNotifier;
final List<StreamSubscription> subscriptions = [];
Offset? _tapPosition;
void _handleTapDown(TapDownDetails details) {
setState(() {
_tapPosition = details.localPosition;
});
}
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
final horizontalGestureSensitivity = 5000;
final verticalGestureSensitivity = 500;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (subscriptions.isEmpty) {
subscriptions.addAll(
[
widget.videoController.player.stream.buffering.listen(
(event) {
setState(() {
buffering = event;
if (event) {
_mountSeekBackwardButton = false;
_mountSeekForwardButton = false;
_hideSeekBackwardButton = false;
_hideSeekForwardButton = false;
}
});
},
),
],
);
_timer = Timer(
controlsHoverDuration,
() {
if (mounted) {
setState(() {
visible = false;
});
}
},
);
}
}
@override
void dispose() {
for (final subscription in subscriptions) {
subscription.cancel();
}
// --------------------------------------------------
// package:screen_brightness
Future.microtask(() async {
try {
await ScreenBrightness().resetScreenBrightness();
} catch (_) {}
});
// --------------------------------------------------
super.dispose();
}
void onTap() {
if (!visible) {
setState(() {
mount = true;
visible = true;
});
_timer?.cancel();
_timer = Timer(controlsHoverDuration, () {
if (mounted) {
setState(() {
visible = false;
});
}
});
} else {
setState(() {
visible = false;
});
_timer?.cancel();
}
}
void onDoubleTapSeekBackward() {
setState(() {
_mountSeekBackwardButton = true;
});
}
void onDoubleTapSeekForward() {
setState(() {
_mountSeekForwardButton = true;
});
}
void onHorizontalDragUpdate(DragUpdateDetails details) {
if (_dragInitialDelta == Offset.zero) {
_dragInitialDelta = details.localPosition;
return;
}
final diff = _dragInitialDelta.dx - details.localPosition.dx;
final duration = widget.videoController.player.state.duration.inSeconds;
final position = widget.videoController.player.state.position.inSeconds;
final seconds = -(diff * duration / horizontalGestureSensitivity).round();
final relativePosition = position + seconds;
if (relativePosition <= duration && relativePosition >= 0) {
setState(() {
swipeDuration = seconds;
showSwipeDuration = true;
_seekBarDeltaValueNotifier = Duration(
seconds: widget.videoController.player.state.position.inSeconds +
seconds);
});
}
}
void onHorizontalDragEnd() {
if (swipeDuration != 0) {
Duration newPosition = widget.videoController.player.state.position +
Duration(seconds: swipeDuration);
newPosition = newPosition.clamp(
Duration.zero,
widget.videoController.player.state.duration,
);
widget.videoController.player.seek(newPosition);
}
setState(() {
_dragInitialDelta = Offset.zero;
showSwipeDuration = false;
_seekBarDeltaValueNotifier = null;
});
}
@override
void initState() {
super.initState();
// --------------------------------------------------
// package:volume_controller
Future.microtask(() async {
try {
VolumeController().showSystemUI = false;
_volumeValue.value = await VolumeController().getVolume();
VolumeController().listener((value) {
if (mounted && !_volumeInterceptEventStream) {
_volumeValue.value = value;
}
});
} catch (_) {}
});
// --------------------------------------------------
// --------------------------------------------------
// package:screen_brightness
Future.microtask(() async {
try {
_brightnessValue.value = await ScreenBrightness().current;
ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
if (mounted) {
_brightnessValue.value = value;
}
});
} catch (_) {}
});
// --------------------------------------------------
}
Future<void> setVolume(double value) async {
// --------------------------------------------------
// package:volume_controller
try {
VolumeController().setVolume(value);
} catch (_) {}
_volumeValue.value = value;
_volumeIndicator.value = true;
_volumeInterceptEventStream = true;
_volumeTimer?.cancel();
_volumeTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
_volumeIndicator.value = false;
_volumeInterceptEventStream = false;
}
});
// --------------------------------------------------
}
Future<void> setBrightness(double value) async {
// --------------------------------------------------
// package:screen_brightness
try {
await ScreenBrightness().setScreenBrightness(value);
} catch (_) {}
_brightnessIndicator.value = true;
_brightnessTimer?.cancel();
_brightnessTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
_brightnessIndicator.value = false;
}
});
// --------------------------------------------------
}
@override
Widget build(BuildContext context) {
return Focus(
autofocus: true,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
// // Controls:
AnimatedOpacity(
curve: Curves.easeInOut,
opacity: visible ? 1.0 : 0.0,
duration: controlsTransitionDuration,
onEnd: () {
setState(() {
if (!visible) {
mount = false;
}
});
},
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Positioned.fill(
child: Container(
color: backdropColor,
),
),
// We are adding 16.0 boundary around the actual controls (which contain the vertical drag gesture detectors).
// This will make the hit-test on edges (e.g. swiping to: show status-bar, show navigation-bar, go back in navigation) not activate the swipe gesture annoyingly.
Positioned.fill(
left: 16.0,
top: 16.0,
right: 16.0,
bottom: 16.0,
child: GestureDetector(
onTap: onTap,
onDoubleTapDown: _handleTapDown,
onDoubleTap: () {
if (_tapPosition != null &&
_tapPosition!.dx >
MediaQuery.of(context).size.width / 2) {
onDoubleTapSeekForward();
} else {
onDoubleTapSeekBackward();
}
},
onHorizontalDragUpdate: (details) {
onHorizontalDragUpdate(details);
},
onHorizontalDragEnd: (details) {
onHorizontalDragEnd();
},
onVerticalDragUpdate: (e) async {
final delta = e.delta.dy;
final Offset position = e.localPosition;
if (position.dx <=
MediaQuery.of(context).size.width / 2) {
// Left side of screen swiped
final brightness = _brightnessValue.value -
delta / verticalGestureSensitivity;
final result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else {
// Right side of screen swiped
final volume = _volumeValue.value -
delta / verticalGestureSensitivity;
final result = volume.clamp(0.0, 1.0);
setVolume(result);
}
},
child: Container(
color: const Color(0x00000000),
),
),
),
if (mount)
Padding(
padding: (
// Add padding in fullscreen!
isFullscreen(context)
? MediaQuery.of(context).padding
: EdgeInsets.zero),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
widget.topButtonBarWidget,
// Only display [primaryButtonBar] if [buffering] is false.
Expanded(
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: buffering
? 0.0
: showSwipeDuration
? 0.0
: 1.0,
duration: controlsTransitionDuration,
child: Center(
child: Row(
children: mobilePrimaryButtonBar(
context,
widget.videoStatekey,
widget.streamController,
widget.videoController)),
),
),
),
Stack(
alignment: Alignment.bottomCenter,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomSeekBar(
onSeekStart: (value) {
setState(() {
swipeDuration = value.inSeconds;
showSwipeDuration = true;
});
_timer?.cancel();
},
onSeekEnd: (value) {
_timer = Timer(
controlsHoverDuration,
() {
if (mounted) {
setState(() {
visible = false;
});
}
},
);
setState(() {
showSwipeDuration = false;
});
},
player: widget.videoController.player,
),
),
widget.bottomButtonBarWidget
],
),
],
),
),
],
),
),
// // Double-Tap Seek Seek-Bar:
if (!mount)
if (_mountSeekBackwardButton ||
_mountSeekForwardButton ||
showSwipeDuration)
Column(
children: [
const Spacer(),
Stack(
alignment: Alignment.bottomCenter,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: CustomSeekBar(
delta: _seekBarDeltaValueNotifier,
player: widget.videoController.player),
),
],
),
],
),
// // Buffering Indicator.
IgnorePointer(
child: Padding(
padding: (
// Add padding in fullscreen!
isFullscreen(context)
? MediaQuery.of(context).padding
: EdgeInsets.zero),
child: Column(
children: [
Container(
height: buttonBarHeight,
margin: const EdgeInsets.all(0),
),
Expanded(
child: Center(
child: TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: buffering ? 1.0 : 0.0,
),
duration: controlsTransitionDuration,
builder: (context, value, child) {
// Only mount the buffering indicator if the opacity is greater than 0.0.
// This has been done to prevent redundant resource usage in [CircularProgressIndicator].
if (value > 0.0) {
return Opacity(
opacity: value,
child: child!,
);
}
return const SizedBox.shrink();
},
child: const CircularProgressIndicator(
color: Color(0xFFFFFFFF),
),
),
),
),
Container(
height: buttonBarHeight,
margin: bottomButtonBarMargin,
),
],
),
),
),
// // Volume Indicator.
IgnorePointer(
child: ValueListenableBuilder(
valueListenable: _volumeIndicator,
builder: (context, value, child) => AnimatedOpacity(
curve: Curves.easeInOut,
opacity: value ? 1.0 : 0.0,
duration: controlsTransitionDuration,
child: MediaIndicatorBuilder(
value: _volumeValue, isVolumeIndicator: true)),
),
),
// // Brightness Indicator.
IgnorePointer(
child: ValueListenableBuilder(
valueListenable: _brightnessIndicator,
builder: (context, value, child) => AnimatedOpacity(
curve: Curves.easeInOut,
opacity: value ? 1.0 : 0.0,
duration: controlsTransitionDuration,
child: MediaIndicatorBuilder(
value: _brightnessValue, isVolumeIndicator: false)),
),
),
// Seek Indicator.
IgnorePointer(
child: AnimatedOpacity(
duration: controlsTransitionDuration,
opacity: showSwipeDuration ? 1 : 0,
child: seekIndicatorTextWidget(Duration(seconds: swipeDuration),
widget.videoController.player.state.position)),
),
// Double-Tap Seek Button(s):
if (_mountSeekBackwardButton || _mountSeekForwardButton)
Positioned.fill(
child: Row(
children: [
Expanded(
child: _mountSeekBackwardButton
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: _hideSeekBackwardButton ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 200),
builder: (context, value, child) => Opacity(
opacity: value,
child: child,
),
onEnd: () {
if (_hideSeekBackwardButton) {
setState(() {
_hideSeekBackwardButton = false;
_mountSeekBackwardButton = false;
});
}
},
child: _BackwardSeekIndicator(
onChanged: (value) {
setState(() {
_seekBarDeltaValueNotifier = widget
.videoController
.player
.state
.position -
value;
});
},
onSubmitted: (value) {
setState(() {
_hideSeekBackwardButton = true;
});
var result = widget
.videoController.player.state.position -
value;
result = result.clamp(
Duration.zero,
widget.videoController.player.state.duration,
);
widget.videoController.player.seek(result);
},
),
)
: const SizedBox(),
),
Expanded(
child: _mountSeekForwardButton
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: _hideSeekForwardButton ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 200),
builder: (context, value, child) => Opacity(
opacity: value,
child: child,
),
onEnd: () {
if (_hideSeekForwardButton) {
setState(() {
_hideSeekForwardButton = false;
_mountSeekForwardButton = false;
});
}
},
child: _ForwardSeekIndicator(
onChanged: (value) {
setState(() {
_seekBarDeltaValueNotifier = widget
.videoController
.player
.state
.position +
value;
});
},
onSubmitted: (value) {
setState(() {
_hideSeekForwardButton = true;
});
var result = widget
.videoController.player.state.position +
value;
result = result.clamp(
Duration.zero,
widget.videoController.player.state.duration,
);
widget.videoController.player.seek(result);
},
),
)
: const SizedBox(),
),
],
),
),
],
),
);
}
}
class _BackwardSeekIndicator extends StatefulWidget {
final void Function(Duration) onChanged;
final void Function(Duration) onSubmitted;
const _BackwardSeekIndicator({
required this.onChanged,
required this.onSubmitted,
});
@override
State<_BackwardSeekIndicator> createState() => _BackwardSeekIndicatorState();
}
class _BackwardSeekIndicatorState extends State<_BackwardSeekIndicator> {
Duration value = const Duration(seconds: 10);
Timer? timer;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void initState() {
super.initState();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
}
void increment() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
widget.onChanged.call(value);
setState(() {
value += const Duration(seconds: 10);
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0x88767676),
Color(0x00767676),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: InkWell(
splashColor: const Color(0x44767676),
onTap: increment,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.fast_rewind,
size: 24.0,
color: Color(0xFFFFFFFF),
),
const SizedBox(height: 8.0),
Text(
'${value.inSeconds} seconds',
style: const TextStyle(
fontSize: 12.0,
color: Color(0xFFFFFFFF),
),
),
],
),
),
),
);
}
}
class _ForwardSeekIndicator extends StatefulWidget {
final void Function(Duration) onChanged;
final void Function(Duration) onSubmitted;
const _ForwardSeekIndicator({
required this.onChanged,
required this.onSubmitted,
});
@override
State<_ForwardSeekIndicator> createState() => _ForwardSeekIndicatorState();
}
class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
Duration value = const Duration(seconds: 10);
Timer? timer;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void initState() {
super.initState();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
}
void increment() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
widget.onChanged.call(value);
setState(() {
value += const Duration(seconds: 10);
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0x00767676),
Color(0x88767676),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: InkWell(
splashColor: const Color(0x44767676),
onTap: increment,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.fast_forward,
size: 24.0,
color: Color(0xFFFFFFFF),
),
const SizedBox(height: 8.0),
Text(
'${value.inSeconds} seconds',
style: const TextStyle(
fontSize: 12.0,
color: Color(0xFFFFFFFF),
),
),
],
),
),
),
);
}
}
// BUTTON: PLAY/PAUSE
/// A material design play/pause button.
class CustomMaterialPlayOrPauseButton extends StatefulWidget {
final VideoController controller;
const CustomMaterialPlayOrPauseButton({
super.key,
required this.controller,
});
@override
CustomMaterialPlayOrPauseButtonState createState() =>
CustomMaterialPlayOrPauseButtonState();
}
class CustomMaterialPlayOrPauseButtonState
extends State<CustomMaterialPlayOrPauseButton>
with SingleTickerProviderStateMixin {
late final animation = AnimationController(
vsync: this,
value: widget.controller.player.state.playing ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
StreamSubscription<bool>? subscription;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
subscription ??= widget.controller.player.stream.playing.listen((event) {
if (event) {
animation.forward();
} else {
animation.reverse();
}
});
}
@override
void dispose() {
animation.dispose();
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: widget.controller.player.playOrPause,
iconSize: 65,
color: Colors.white,
icon: IgnorePointer(
child: AnimatedIcon(
progress: animation,
icon: AnimatedIcons.play_pause,
size: 65,
color: Colors.white,
),
),
);
}
}
List<Widget> mobilePrimaryButtonBar(
BuildContext context,
GlobalKey<VideoState> key,
AnimeStreamController streamController,
VideoController controller) {
bool hasPrevEpisode = streamController.getEpisodeIndex().$1 + 1 !=
streamController.getEpisodesLength(streamController.getEpisodeIndex().$2);
bool hasNextEpisode = streamController.getEpisodeIndex().$1 != 0;
final isFullScreen = isFullscreen(context);
return [
const Spacer(flex: 3),
IconButton(
onPressed: hasPrevEpisode
? () {
if (isFullScreen) {
key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context, chapter: streamController.getPrevEpisode());
}
: null,
icon: Icon(
Icons.skip_previous,
size: 35,
color: hasPrevEpisode ? Colors.white : Colors.grey,
),
),
const Spacer(),
CustomMaterialPlayOrPauseButton(controller: controller),
const Spacer(),
IconButton(
onPressed: hasNextEpisode
? () {
if (isFullScreen) {
key.currentState?.exitFullscreen();
}
pushReplacementMangaReaderView(
context: context,
chapter: streamController.getNextEpisode(),
);
}
: null,
icon: Icon(Icons.skip_next,
size: 35, color: hasPrevEpisode ? Colors.white : Colors.grey),
),
const Spacer(flex: 3)
];
}

View file

@ -17,9 +17,7 @@ class SendvidExtractor {
final masterUrl =
document.querySelector("source#video_source")?.attributes["src"];
if (masterUrl == null) {
return videoList;
}
if (masterUrl == null) return videoList;
final masterHeaders = Map<String, String>.from(headers)
..addAll({
@ -34,7 +32,7 @@ class SendvidExtractor {
final masterPlaylist = masterPlaylistResponse.body;
final masterBase =
"https://${Uri.parse(masterUrl).host}${Uri.parse(masterUrl).pathSegments.join("/")}/";
"${"https://${Uri.parse(masterUrl).host}${Uri.parse(masterUrl).path}".substringBeforeLast("/")}/";
masterPlaylist
.substringAfter("#EXT-X-STREAM-INF:")
@ -52,9 +50,8 @@ class SendvidExtractor {
"Origin": "https://${Uri.parse(url).host}",
"Referer": "https://${Uri.parse(url).host}/",
});
videoList.add(
Video(videoUrl, "$prefix - $quality", videoUrl, headers: videoHeaders));
videoList.add(Video(videoUrl, "$prefix - $quality", videoUrl,
headers: videoHeaders));
});
return videoList;

View file

@ -5,7 +5,7 @@ import 'package:mangayomi/utils/extensions.dart';
class SibnetExtractor {
final http.Client client = http.Client();
Future<List<Video>> videosFromUrl(String url) async {
Future<List<Video>> videosFromUrl(String url, {String prefix = ""}) async {
List<Video> videoList = [];
try {
final response = await client.get(Uri.parse(url));
@ -28,7 +28,7 @@ class SibnetExtractor {
};
videoList.add(
Video(videoUrl, "Sibnet", videoUrl, headers: videoHeaders),
Video(videoUrl, "$prefix - Sibnet", videoUrl, headers: videoHeaders),
);
return videoList;

View file

@ -10,7 +10,9 @@
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -26,9 +28,15 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
g_autoptr(FlPluginRegistrar) window_to_front_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin");
window_to_front_plugin_register_with_registrar(window_to_front_registrar);

View file

@ -7,7 +7,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
isar_flutter_libs
media_kit_libs_linux
media_kit_video
screen_retriever
url_launcher_linux
window_manager
window_to_front
)

View file

@ -13,10 +13,12 @@ import media_kit_video
import package_info_plus
import path_provider_foundation
import screen_brightness_macos
import screen_retriever
import share_plus
import sqflite
import url_launcher_macos
import wakelock_plus
import window_manager
import window_to_front
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -28,9 +30,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
}

View file

@ -1215,6 +1215,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.3"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
scrollable_positioned_list:
dependency: "direct main"
description:
@ -1540,6 +1548,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.0"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7
url: "https://pub.dev"
source: hosted
version: "0.3.7"
window_to_front:
dependency: transitive
description:

View file

@ -63,6 +63,7 @@ dependencies:
rinf: ^4.19.0
protobuf: ^3.1.0
cupertino_icons: ^1.0.2
window_manager: ^0.3.7
dependency_overrides:

View file

@ -12,8 +12,10 @@
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -29,10 +31,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
WindowToFrontPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowToFrontPlugin"));
}

View file

@ -9,8 +9,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video
permission_handler_windows
screen_brightness_windows
screen_retriever
share_plus
url_launcher_windows
window_manager
window_to_front
)