Fix player views
This commit is contained in:
parent
a7773f9435
commit
710c498bb1
18 changed files with 2131 additions and 483 deletions
|
|
@ -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())))),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -168,4 +168,5 @@ class AnimeStreamController extends _$AnimeStreamController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
821
lib/modules/anime/widgets/desktop.dart
Normal file
821
lib/modules/anime/widgets/desktop.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
933
lib/modules/anime/widgets/mobile.dart
Normal file
933
lib/modules/anime/widgets/mobile.dart
Normal 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)
|
||||
];
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
16
pubspec.lock
16
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue