diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8663f02..bdebb62 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -22,6 +22,10 @@ if (keystorePropertiesFile.exists()) { } android { + packaging { + jniLibs.pickFirsts.add("lib/**/libc++_shared.so") + } + namespace = "com.remi.piwigo_ng" //compileSdkVersion flutter.compileSdkVersion //ndkVersion = flutter.ndkVersion @@ -65,6 +69,15 @@ android { buildTypes { release { + // Compress libs + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..b714dae --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class org.videolan.libvlc.** { *; } \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index fb08499..447ccd7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -17,7 +17,7 @@ import 'package:piwigo_ng/views/image/image_favorites_page.dart'; import 'package:piwigo_ng/views/image/image_page.dart'; import 'package:piwigo_ng/views/image/image_search_page.dart'; import 'package:piwigo_ng/views/image/image_tags_page.dart'; -import 'package:piwigo_ng/views/image/video_player_page.dart'; +import 'package:piwigo_ng/views/image/vlc_video_player.dart'; import 'package:piwigo_ng/views/settings/auto_upload_page.dart'; import 'package:piwigo_ng/views/settings/privacy_policy_page.dart'; import 'package:piwigo_ng/views/settings/select_language_page.dart'; @@ -192,9 +192,9 @@ Route generateRoute(RouteSettings settings) { ), settings: settings, ); - case VideoPlayerPage.routeName: + case VlcVideoPlayer.routeName: return MaterialPageRoute( - builder: (_) => VideoPlayerPage( + builder: (_) => VlcVideoPlayer( videoUrl: arguments['videoUrl'], thumbnailUrl: arguments['thumbnailUrl'], ), diff --git a/lib/components/cards/image_details_card.dart b/lib/components/cards/image_details_card.dart index f078c66..8ee8797 100644 --- a/lib/components/cards/image_details_card.dart +++ b/lib/components/cards/image_details_card.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' as ui show Image; import 'package:auto_size_text/auto_size_text.dart'; @@ -14,10 +15,11 @@ import 'package:piwigo_ng/services/preferences_service.dart'; import 'package:piwigo_ng/utils/resources.dart'; import 'package:piwigo_ng/utils/settings.dart'; import 'package:provider/provider.dart'; -import 'package:video_player/video_player.dart'; +import 'package:flutter_video_thumbnail_plus/flutter_video_thumbnail_plus.dart'; class ImageDetailsCard extends StatelessWidget { - const ImageDetailsCard({Key? key, required this.image, this.onRemove}) : super(key: key); + const ImageDetailsCard({Key? key, required this.image, this.onRemove}) + : super(key: key); final ImageModel image; final Function()? onRemove; @@ -56,7 +58,9 @@ class ImageDetailsCard extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(5.0), child: Builder(builder: (context) { - final String? imageUrl = image.getDerivativeFromString(Preferences.getImageThumbnailSize)?.url; + final String? imageUrl = image + .getDerivativeFromString(Preferences.getImageThumbnailSize) + ?.url; return ImageNetworkDisplay( imageUrl: imageUrl, ); @@ -88,7 +92,10 @@ class ImageDetailsCard extends StatelessWidget { children: [ Flexible( child: Text( - image.file.replaceAll('', '\u200B').split(path.extension(image.file)).first, + image.file + .replaceAll('', '\u200B') + .split(path.extension(image.file)) + .first, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, @@ -106,11 +113,13 @@ class ImageDetailsCard extends StatelessWidget { const Spacer(), if (image.dateAvailable != null) Builder(builder: (context) { - LocaleNotifier localeNotifier = Provider.of(context, listen: false); + LocaleNotifier localeNotifier = + Provider.of(context, listen: false); String date = - DateFormat.yMMMMd(localeNotifier.locale.languageCode).format(DateTime.parse(image.dateAvailable!)); - String time = - DateFormat.Hms(localeNotifier.locale.languageCode).format(DateTime.parse(image.dateAvailable!)); + DateFormat.yMMMMd(localeNotifier.locale.languageCode) + .format(DateTime.parse(image.dateAvailable!)); + String time = DateFormat.Hms(localeNotifier.locale.languageCode) + .format(DateTime.parse(image.dateAvailable!)); return AutoSizeText( "$date $time", maxLines: 1, @@ -142,7 +151,8 @@ class ImageDetailsCard extends StatelessWidget { } class LocalImageDetailsCard extends StatefulWidget { - const LocalImageDetailsCard({Key? key, required this.image, this.onRemove, this.isDuplicate = false}) + const LocalImageDetailsCard( + {Key? key, required this.image, this.onRemove, this.isDuplicate = false}) : super(key: key); final File image; @@ -205,12 +215,17 @@ class _LocalImageDetailsCardState extends State { fit: StackFit.expand, children: [ LayoutBuilder(builder: (context, constraints) { - List? mimeType = mime(widget.image.path.split('/').last)?.split('/'); + List? mimeType = + mime(widget.image.path.split('/').last)?.split('/'); if (mimeType?.first == 'image') { _checkMemory(); - double? cacheWidth = constraints.maxWidth.isInfinite ? constraints.maxWidth : null; - double? cacheHeight = constraints.maxHeight.isInfinite ? constraints.maxHeight : null; + double? cacheWidth = constraints.maxWidth.isInfinite + ? constraints.maxWidth + : null; + double? cacheHeight = constraints.maxHeight.isInfinite + ? constraints.maxHeight + : null; return Image.file( widget.image, fit: BoxFit.cover, @@ -313,7 +328,8 @@ class _LocalImageDetailsCardState extends State { } class LocalVideoDetailsCard extends StatefulWidget { - const LocalVideoDetailsCard({Key? key, required this.video, this.onRemove, this.isDuplicate = false}) + const LocalVideoDetailsCard( + {Key? key, required this.video, this.onRemove, this.isDuplicate = false}) : super(key: key); final File video; @@ -325,31 +341,28 @@ class LocalVideoDetailsCard extends StatefulWidget { } class _LocalVideoDetailsCardState extends State { - late final VideoPlayerController _controller; + late Image thumbnail; @override void initState() { - _controller = VideoPlayerController.file( - File(widget.video.path), - videoPlayerOptions: VideoPlayerOptions(), - )..initialize().then((_) => setState(() {})); + WidgetsBinding.instance.addPostFrameCallback((_) async { + thumbnail =Image.memory(Uint8List(0)); + thumbnail = Image.memory(await FlutterVideoThumbnailPlus.thumbnailData( + video: widget.video.path) ?? + ([] as Uint8List)); + }); super.initState(); } + Future getThumbnail() async { + setState(() {}); + } + @override void dispose() { - _controller.dispose(); super.dispose(); } - String get _duration { - final Duration duration = _controller.value.duration; - int hours = duration.inHours; - int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; - return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; - } - @override Widget build(BuildContext context) { return Stack( @@ -385,16 +398,6 @@ class _LocalVideoDetailsCardState extends State { fit: StackFit.expand, children: [ LayoutBuilder(builder: (context, constraints) { - if (_controller.value.hasError) { - return Center( - child: Icon(Icons.image_not_supported), - ); - } - if (!_controller.value.isInitialized) { - return Center( - child: CircularProgressIndicator(), - ); - } return Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -403,12 +406,9 @@ class _LocalVideoDetailsCardState extends State { child: FittedBox( fit: BoxFit.cover, child: SizedBox( - width: _controller.value.size.width, - height: _controller.value.size.height, - child: AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: VideoPlayer(_controller), - ), + width: thumbnail.width, + height: thumbnail.height, + child: thumbnail, ), ), ), @@ -416,12 +416,17 @@ class _LocalVideoDetailsCardState extends State { bottom: 2.0, left: 2.0, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), color: AppColors.black.withValues(alpha: 0.7)), + borderRadius: BorderRadius.circular(5), + color: AppColors.black.withValues(alpha: 0.7)), child: Text( - _duration, - style: TextStyle(color: AppColors.white, fontSize: 10, fontWeight: FontWeight.bold), + "duration", + style: TextStyle( + color: AppColors.white, + fontSize: 10, + fontWeight: FontWeight.bold), ), ), ), @@ -452,21 +457,11 @@ class _LocalVideoDetailsCardState extends State { ), child: Builder( builder: (context) { - if (_controller.value.hasError) { - return Center( - child: Icon(Icons.image_not_supported), - ); - } - if (!_controller.value.isInitialized) { - return Center( - child: CircularProgressIndicator(), - ); - } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - "${_controller.value.size.width.round()}x${_controller.value.size.height.round()} pixels", + "${thumbnail.width}x${thumbnail.height} pixels", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, diff --git a/lib/components/modals/select_move_or_copy_modal.dart b/lib/components/modals/select_move_or_copy_modal.dart index ac64c4c..967b787 100644 --- a/lib/components/modals/select_move_or_copy_modal.dart +++ b/lib/components/modals/select_move_or_copy_modal.dart @@ -4,7 +4,6 @@ import 'package:piwigo_ng/models/album_model.dart'; import 'package:piwigo_ng/network/albums.dart'; import 'package:piwigo_ng/network/api_error.dart'; import 'package:piwigo_ng/utils/localizations.dart'; -import 'package:piwigo_ng/utils/resources.dart'; class SelectMoveOrCopyModal extends StatefulWidget { const SelectMoveOrCopyModal({ diff --git a/lib/components/player_controls.dart b/lib/components/player_controls.dart deleted file mode 100644 index 67ba1d9..0000000 --- a/lib/components/player_controls.dart +++ /dev/null @@ -1,728 +0,0 @@ -import 'dart:async'; - -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:piwigo_ng/services/player_provider.dart'; -import 'package:provider/provider.dart'; -import 'package:video_player/video_player.dart'; - -class PlayerControls extends StatefulWidget { - const PlayerControls({ - Key? key, - this.showPlayButton = true, - this.onToggleOverlay, - }) : super(key: key); - - final Function(bool)? onToggleOverlay; - final bool showPlayButton; - - @override - State createState() { - return _PlayerControlsState(); - } -} - -class _PlayerControlsState extends State - with SingleTickerProviderStateMixin { - late PlayerProvider notifier; - late VideoPlayerValue _latestValue; - double? _latestVolume; - Timer? _hideTimer; - Timer? _initTimer; - late var _subtitlesPosition = Duration.zero; - bool _subtitleOn = false; - Timer? _showAfterExpandCollapseTimer; - bool _dragging = false; - bool _displayTapped = false; - Timer? _bufferingDisplayTimer; - bool _displayBufferingIndicator = false; - - final barHeight = 48.0 * 1.5; - final marginSize = 5.0; - - late VideoPlayerController controller; - ChewieController? _chewieController; - - // We know that _chewieController is set in didChangeDependencies - ChewieController get chewieController => _chewieController!; - - @override - void initState() { - super.initState(); - notifier = Provider.of(context, listen: false); - } - - @override - void didChangeDependencies() { - final oldController = _chewieController; - _chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; - - if (oldController != chewieController) { - _dispose(); - _initialize(); - } - - super.didChangeDependencies(); - } - - @override - void dispose() { - _dispose(); - super.dispose(); - } - - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - _initTimer?.cancel(); - _showAfterExpandCollapseTimer?.cancel(); - } - - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - _startHideTimer(); - - setState(() { - notifier.hideStuff = false; - widget.onToggleOverlay?.call(true); - _displayTapped = true; - }); - } - - Future _initialize() async { - _subtitleOn = chewieController.subtitle?.isNotEmpty ?? false; - controller.addListener(_updateState); - - _updateState(); - - if (controller.value.isPlaying || chewieController.autoPlay) { - _startHideTimer(); - } - - if (chewieController.showControlsOnInitialize) { - _initTimer = Timer(const Duration(milliseconds: 200), () { - setState(() { - notifier.hideStuff = false; - widget.onToggleOverlay?.call(true); - }); - }); - } - } - - void _onExpandCollapse() { - setState(() { - notifier.hideStuff = true; - widget.onToggleOverlay?.call(false); - - chewieController.toggleFullScreen(); - _showAfterExpandCollapseTimer = - Timer(const Duration(milliseconds: 300), () { - setState(() { - _cancelAndRestartTimer(); - }); - }); - }); - } - - void _playPause() { - final isFinished = _latestValue.position >= _latestValue.duration; - - setState(() { - if (controller.value.isPlaying) { - notifier.hideStuff = false; - widget.onToggleOverlay?.call(true); - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); - - if (!controller.value.isInitialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(Duration.zero); - } - controller.play(); - } - } - }); - } - - void _startHideTimer() { - final hideControlsTimer = chewieController.hideControlsTimer.isNegative - ? ChewieController.defaultHideControlsTimer - : chewieController.hideControlsTimer; - _hideTimer = Timer(hideControlsTimer, () { - setState(() { - notifier.hideStuff = true; - widget.onToggleOverlay?.call(false); - }); - }); - } - - void _bufferingTimerTimeout() { - _displayBufferingIndicator = true; - if (mounted) { - setState(() {}); - } - } - - void _updateState() { - if (!mounted) return; - - // display the progress bar indicator only after the buffering delay if it has been set - if (chewieController.progressIndicatorDelay != null) { - if (controller.value.isBuffering) { - _bufferingDisplayTimer ??= Timer( - chewieController.progressIndicatorDelay!, - _bufferingTimerTimeout, - ); - } else { - _bufferingDisplayTimer?.cancel(); - _bufferingDisplayTimer = null; - _displayBufferingIndicator = false; - } - } else { - _displayBufferingIndicator = controller.value.isBuffering; - } - - setState(() { - _latestValue = controller.value; - _subtitlesPosition = controller.value.position; - }); - } - - String formatDuration(Duration position) { - final ms = position.inMilliseconds; - - int seconds = ms ~/ 1000; - final int hours = seconds ~/ 3600; - seconds = seconds % 3600; - final minutes = seconds ~/ 60; - seconds = seconds % 60; - - final hoursString = hours >= 10 - ? '$hours' - : hours == 0 - ? '00' - : '0$hours'; - - final minutesString = minutes >= 10 - ? '$minutes' - : minutes == 0 - ? '00' - : '0$minutes'; - - final secondsString = seconds >= 10 - ? '$seconds' - : seconds == 0 - ? '00' - : '0$seconds'; - - final formattedTime = - '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; - - return formattedTime; - } - - @override - Widget build(BuildContext context) { - if (_latestValue.hasError) { - return chewieController.errorBuilder?.call( - context, - chewieController.videoPlayerController.value.errorDescription!, - ) ?? - const Center( - child: Icon( - Icons.error, - color: Colors.white, - size: 42, - ), - ); - } - - return GestureDetector( - onTap: () => _cancelAndRestartTimer(), - child: AbsorbPointer( - absorbing: notifier.hideStuff, - child: Stack( - children: [ - Positioned.fill( - child: IgnorePointer( - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - opacity: notifier.hideStuff ? 0.0 : 1.0, - child: Material( - color: Colors.black.withValues(alpha: 0.5), - ), - ), - ), - ), - if (_displayBufferingIndicator) - const Center( - child: CircularProgressIndicator(), - ) - else - _buildHitArea(), - Align( - alignment: Alignment.bottomCenter, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (_subtitleOn) - Transform.translate( - offset: Offset( - 0.0, - notifier.hideStuff ? barHeight * 0.8 : 0.0, - ), - child: - _buildSubtitles(context, chewieController.subtitle!), - ), - _buildBottomBar(context), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildSubtitles(BuildContext context, Subtitles subtitles) { - if (!_subtitleOn) { - return const SizedBox(); - } - final currentSubtitle = subtitles.getByPosition(_subtitlesPosition); - if (currentSubtitle.isEmpty) { - return const SizedBox(); - } - - if (chewieController.subtitleBuilder != null) { - return chewieController.subtitleBuilder!( - context, - currentSubtitle.first!.text, - ); - } - - return Padding( - padding: EdgeInsets.all(marginSize), - child: Container( - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - color: const Color(0x96000000), - borderRadius: BorderRadius.circular(10.0), - ), - child: Text( - currentSubtitle.first!.text.toString(), - style: const TextStyle( - fontSize: 18, - ), - textAlign: TextAlign.center, - ), - ), - ); - } - - Widget _buildBottomBar( - BuildContext context, - ) { - final iconColor = Theme.of(context).textTheme.labelLarge!.color; - return AnimatedOpacity( - opacity: notifier.hideStuff ? 0.0 : 1.0, - duration: const Duration(milliseconds: 300), - child: Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: SafeArea( - bottom: chewieController.isFullScreen, - top: false, - minimum: chewieController.controlsSafeAreaMinimum, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildPosition(iconColor), - if (chewieController.allowMuting) - _buildMuteButton(controller), - const Spacer(), - if (chewieController.allowFullScreen) _buildExpandButton(), - ], - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: _buildProgressBar(), - ), - ], - ), - ), - ), - ); - } - - Widget _buildMuteButton( - VideoPlayerController controller, - ) { - return AnimatedOpacity( - opacity: notifier.hideStuff ? 0.0 : 1.0, - duration: const Duration(milliseconds: 300), - child: IconButton( - color: Colors.white, - icon: Icon(Icons.volume_up), - selectedIcon: Icon(Icons.volume_off), - isSelected: _latestValue.volume == 0, - onPressed: () { - _cancelAndRestartTimer(); - if (_latestValue.volume == 0) { - controller.setVolume(_latestVolume ?? 0.5); - } else { - _latestVolume = controller.value.volume; - controller.setVolume(0.0); - } - }, - ), - ); - } - - Widget _buildExpandButton() { - return AnimatedOpacity( - opacity: notifier.hideStuff ? 0.0 : 1.0, - duration: const Duration(milliseconds: 300), - child: IconButton( - color: Colors.white, - onPressed: _onExpandCollapse, - icon: Icon(Icons.fullscreen), - selectedIcon: Icon(Icons.fullscreen_exit), - isSelected: chewieController.isFullScreen, - ), - ); - } - - Widget _buildHitArea() { - final bool isFinished = _latestValue.position >= _latestValue.duration; - final bool showPlayButton = - widget.showPlayButton && !_dragging && !notifier.hideStuff; - - return GestureDetector( - onTap: () { - if (_latestValue.isPlaying) { - if (_displayTapped) { - setState(() { - notifier.hideStuff = true; - widget.onToggleOverlay?.call(false); - }); - } else { - _cancelAndRestartTimer(); - } - } else { - setState(() { - notifier.hideStuff = true; - widget.onToggleOverlay?.call(false); - }); - } - }, - child: ColoredBox( - color: Colors.transparent, - child: Center( - child: GestureDetector( - onTap: () { - _playPause(); - }, - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: showPlayButton ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black54, - shape: BoxShape.circle, - ), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: Colors.white) - : controller.value.isPlaying - ? Icon(Icons.pause, color: Colors.white) - : Icon(Icons.play_arrow, color: Colors.white), - onPressed: _playPause, - ), - ), - ), - ), - ), - ), - ), - ); - } - - Widget _buildPosition(Color? iconColor) { - final position = _latestValue.position; - final duration = _latestValue.duration; - - return RichText( - text: TextSpan( - text: '${formatDuration(position)} ', - children: [ - TextSpan( - text: '/ ${formatDuration(duration)}', - style: TextStyle( - fontSize: 14.0, - color: Colors.white.withValues(alpha: 0.75), - fontWeight: FontWeight.normal, - ), - ) - ], - style: const TextStyle( - fontSize: 14.0, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); - } - - Widget _buildProgressBar() { - return VideoProgressBar( - controller, - barHeight: 4, - handleHeight: 8, - drawShadow: true, - onDragStart: () { - setState(() { - _dragging = true; - }); - _hideTimer?.cancel(); - }, - onDragEnd: () { - setState(() { - _dragging = false; - }); - _startHideTimer(); - }, - colors: ChewieProgressColors( - playedColor: Theme.of(context).colorScheme.secondary.withValues(alpha: 0.7), - handleColor: Theme.of(context).colorScheme.secondary, - bufferedColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), - backgroundColor: Theme.of(context).disabledColor.withValues(alpha: 0.3), - ), - ); - } -} - -class VideoProgressBar extends StatefulWidget { - VideoProgressBar( - this.controller, { - ChewieProgressColors? colors, - this.onDragEnd, - this.onDragStart, - this.onDragUpdate, - Key? key, - required this.barHeight, - required this.handleHeight, - required this.drawShadow, - }) : colors = colors ?? ChewieProgressColors(), - super(key: key); - - final VideoPlayerController controller; - final ChewieProgressColors colors; - final Function()? onDragStart; - final Function()? onDragEnd; - final Function()? onDragUpdate; - - final double barHeight; - final double handleHeight; - final bool drawShadow; - - @override - // ignore: library_private_types_in_public_api - _VideoProgressBarState createState() { - return _VideoProgressBarState(); - } -} - -class _VideoProgressBarState extends State { - void listener() { - if (!mounted) return; - setState(() {}); - } - - bool _controllerWasPlaying = false; - - VideoPlayerController get controller => widget.controller; - - @override - void initState() { - super.initState(); - controller.addListener(listener); - } - - @override - void deactivate() { - controller.removeListener(listener); - super.deactivate(); - } - - void _seekToRelativePosition(Offset globalPosition) { - final box = context.findRenderObject()! as RenderBox; - final Offset tapPos = box.globalToLocal(globalPosition); - final double relative = tapPos.dx / box.size.width; - final Duration position = controller.value.duration * relative; - controller.seekTo(position); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onHorizontalDragStart: (DragStartDetails details) { - if (!controller.value.isInitialized) { - return; - } - _controllerWasPlaying = controller.value.isPlaying; - if (_controllerWasPlaying) { - controller.pause(); - } - - widget.onDragStart?.call(); - }, - onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.isInitialized) { - return; - } - // Should only seek if the VideoPlayerController cannot be buffering. - // final shouldSeekToRelativePosition = !controller.value.isBuffering; - // if (shouldSeekToRelativePosition) { - // _seekToRelativePosition(details.globalPosition); - // } - _seekToRelativePosition(details.globalPosition); - - widget.onDragUpdate?.call(); - }, - onHorizontalDragEnd: (DragEndDetails details) { - if (_controllerWasPlaying) { - controller.play(); - } - - widget.onDragEnd?.call(); - }, - onTapDown: (TapDownDetails details) { - if (!controller.value.isInitialized) { - return; - } - _seekToRelativePosition(details.globalPosition); - }, - child: Center( - child: Container( - padding: EdgeInsets.symmetric(vertical: 8.0), - width: MediaQuery.of(context).size.width, - color: Colors.transparent, - child: CustomPaint( - painter: _ProgressBarPainter( - value: controller.value, - colors: widget.colors, - barHeight: widget.barHeight, - handleHeight: widget.handleHeight, - drawShadow: widget.drawShadow, - ), - ), - ), - ), - ); - } -} - -class _ProgressBarPainter extends CustomPainter { - _ProgressBarPainter({ - required this.value, - required this.colors, - required this.barHeight, - required this.handleHeight, - required this.drawShadow, - }); - - VideoPlayerValue value; - ChewieProgressColors colors; - - final double barHeight; - final double handleHeight; - final bool drawShadow; - - @override - bool shouldRepaint(CustomPainter painter) { - return true; - } - - @override - void paint(Canvas canvas, Size size) { - final baseOffset = size.height / 2 - barHeight / 2; - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, baseOffset), - Offset(size.width, baseOffset + barHeight), - ), - const Radius.circular(4.0), - ), - colors.backgroundPaint, - ); - if (!value.isInitialized) { - return; - } - final double playedPartPercent = - value.position.inMilliseconds / value.duration.inMilliseconds; - final double playedPart = - playedPartPercent > 1 ? size.width : playedPartPercent * size.width; - for (final DurationRange range in value.buffered) { - final double start = range.startFraction(value.duration) * size.width; - final double end = range.endFraction(value.duration) * size.width; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(start, baseOffset), - Offset(end, baseOffset + barHeight), - ), - const Radius.circular(4.0), - ), - colors.bufferedPaint, - ); - } - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, baseOffset), - Offset(playedPart, baseOffset + barHeight), - ), - const Radius.circular(4.0), - ), - colors.playedPaint, - ); - - if (drawShadow) { - final Path shadowPath = Path() - ..addOval( - Rect.fromCircle( - center: Offset(playedPart, baseOffset + barHeight / 2), - radius: handleHeight, - ), - ); - - canvas.drawShadow(shadowPath, Colors.black, 0.2, false); - } - - canvas.drawCircle( - Offset(playedPart, baseOffset + barHeight / 2), - handleHeight, - colors.handlePaint, - ); - } -} diff --git a/lib/views/image/image_page.dart b/lib/views/image/image_page.dart index 69c206b..07f43af 100644 --- a/lib/views/image/image_page.dart +++ b/lib/views/image/image_page.dart @@ -23,7 +23,7 @@ import 'package:piwigo_ng/utils/image_actions.dart'; import 'package:piwigo_ng/utils/localizations.dart'; import 'package:piwigo_ng/utils/resources.dart'; import 'package:piwigo_ng/utils/settings.dart'; -import 'package:piwigo_ng/views/image/video_player_page.dart'; +import 'package:piwigo_ng/views/image/vlc_video_player.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -511,7 +511,7 @@ class _ImagePageState extends State { ), onPressed: () { Navigator.of(context).pushNamed( - VideoPlayerPage.routeName, + VlcVideoPlayer.routeName, arguments: { 'videoUrl': image.elementUrl, 'thumbnailUrl': imageUrl, diff --git a/lib/views/image/video_player_page.dart b/lib/views/image/video_player_page.dart deleted file mode 100644 index 0c903d0..0000000 --- a/lib/views/image/video_player_page.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:piwigo_ng/components/player_controls.dart'; -import 'package:piwigo_ng/services/player_provider.dart'; -import 'package:piwigo_ng/utils/localizations.dart'; -import 'package:provider/provider.dart'; -import 'package:video_player/video_player.dart'; - -class VideoPlayerPage extends StatefulWidget { - const VideoPlayerPage({ - Key? key, - this.videoUrl, - this.thumbnailUrl, - }) : super(key: key); - - static const String routeName = '/image/video_player'; - - final String? videoUrl; - final String? thumbnailUrl; - - @override - State createState() => _VideoPlayerPageState(); -} - -class _VideoPlayerPageState extends State { - /// Duration of overlay animation - final Duration _overlayAnimationDuration = const Duration(milliseconds: 300); - - /// Curve of overlay animation - final Curve _overlayAnimationCurve = Curves.ease; - - late VideoPlayerController _videoPlayerController; - late PlayerProvider _provider; - ChewieController? _chewieController; - bool _showOverlay = true; - - @override - void initState() { - super.initState(); - initializePlayer(); - _provider = PlayerProvider.init(true); - } - - @override - void dispose() { - _videoPlayerController.dispose(); - _chewieController?.dispose(); - super.dispose(); - } - - void _onToggleOverlay([bool? value]) { - setState(() { - if (value != null) { - _showOverlay = value; - } else { - _showOverlay = !_showOverlay; - } - }); - } - - Future initializePlayer() async { - if (widget.videoUrl == null) return; - _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl!)); - await _videoPlayerController.initialize(); - _createChewieController(); - setState(() {}); - } - - void _createChewieController() { - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController, - autoPlay: true, - allowFullScreen: false, - showControlsOnInitialize: true, - showOptions: false, - hideControlsTimer: const Duration(seconds: 3), - customControls: PlayerControls(onToggleOverlay: _onToggleOverlay), - )..setVolume(0); - } - - @override - Widget build(BuildContext context) { - if (widget.videoUrl == null) { - return Center( - child: Text(appStrings.errorHUD_label), - ); - } - if (_chewieController == null || !_chewieController!.videoPlayerController.value.isInitialized) { - return Center( - child: CircularProgressIndicator(), - ); - } - return Scaffold( - backgroundColor: Colors.black, - body: ChangeNotifierProvider.value( - value: _provider, - child: Stack( - children: [ - Positioned.fill( - child: Chewie( - controller: _chewieController!, - ), - ), - Align( - alignment: Alignment.topLeft, - child: AnimatedSlide( - duration: _overlayAnimationDuration, - curve: _overlayAnimationCurve, - offset: _showOverlay ? Offset.zero : Offset(-1, 0), - child: AnimatedOpacity( - duration: _overlayAnimationDuration, - curve: _overlayAnimationCurve, - opacity: _showOverlay ? 1 : 0, - child: SafeArea( - child: IconButton( - color: Theme.of(context).colorScheme.secondary, - onPressed: () => Navigator.of(context).pop(), - icon: Icon(Icons.close), - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/views/image/video_view.dart b/lib/views/image/video_view.dart deleted file mode 100644 index 0d00b70..0000000 --- a/lib/views/image/video_view.dart +++ /dev/null @@ -1,523 +0,0 @@ -import 'dart:math'; - -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:piwigo_ng/components/player_controls.dart'; -import 'package:piwigo_ng/services/player_provider.dart'; -import 'package:piwigo_ng/utils/localizations.dart'; -import 'package:piwigo_ng/utils/resources.dart'; -import 'package:piwigo_ng/utils/themes.dart'; -import 'package:provider/provider.dart'; -import 'package:video_player/video_player.dart'; - -class VideoView extends StatefulWidget { - const VideoView({ - Key? key, - this.videoUrl, - this.thumbnailUrl, - this.onToggleOverlay, - this.showOverlay = false, - this.screenPadding = EdgeInsets.zero, - }) : super(key: key); - - final String? videoUrl; - final String? thumbnailUrl; - final Function(bool)? onToggleOverlay; - final bool showOverlay; - final EdgeInsets screenPadding; - - @override - State createState() => _VideoViewState(); -} - -class _VideoViewState extends State { - final Duration _overlayAnimationDuration = const Duration(milliseconds: 300); - final Curve _overlayAnimationCurve = Curves.ease; - late VideoPlayerController _controller; - double _progress = 0; - double _updateProgressInterval = 0.0; - bool _mute = false; - bool _isEnd = false; - - /// Initialize video controller - @override - void initState() { - super.initState(); - _controller = VideoPlayerController.networkUrl( - Uri.parse(widget.videoUrl!), - videoPlayerOptions: VideoPlayerOptions(), - )..initialize().then((_) { - debugPrint("---- controller initialized"); - _controller.addListener(_onControllerUpdated); - _controller.addListener(_checkControllerEnd); - setState(() {}); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - /// Update video progress - /// * Listen to [_controller] - void _onControllerUpdated() async { - if (!mounted) return; - // blocking too many updates - // important !! - final int now = DateTime.now().millisecondsSinceEpoch; - if (_updateProgressInterval > now) return; - _updateProgressInterval = now + 300.0; - - final VideoPlayerController controller = _controller; - - if (!controller.value.isInitialized) return; - - // handle progress indicator - if (controller.value.isPlaying) { - final Duration position = controller.value.position; - if (!mounted) return; - setState(() { - _progress = position.inMilliseconds.ceilToDouble() / controller.value.duration.inMilliseconds.ceilToDouble(); - }); - } - } - - /// Check and handle video end - /// * Listen to [_controller] - void _checkControllerEnd() async { - if (!mounted) return; - if (_controller.value.position.inMilliseconds > 0 && - _controller.value.position.inSeconds >= _controller.value.duration.inSeconds) { - setState(() { - _isEnd = true; - if (!widget.showOverlay) { - widget.onToggleOverlay?.call(true); - } - setState(() { - _progress = 1; - }); - }); - } else { - if (_isEnd) { - setState(() { - _isEnd = false; - }); - } - } - } - - /// Fast rewind action - /// * Player rewind by 5sec - Future _onFastRewind() async { - // Pause player and update overlay - setState(() { - _controller.pause(); - }); - // Rewind 5sec on the player - await _controller.seekTo(_controller.value.position - Duration(seconds: 5)); - // Resume player and update overlay - setState(() { - _controller.play(); - }); - } - - /// Play / pause action - /// * If playing: pause the video - /// * Otherwise: resume / play the video - void _onPlayPause() { - setState(() { - if (_controller.value.isPlaying) { - _controller.pause(); - } else { - _controller.play(); - } - }); - } - - /// Fast forward action - /// * Player forward by 5sec - Future _onFastForward() async { - // Pause player and update overlay - setState(() { - _controller.pause(); - }); - // Rewind 5sec on the player - await _controller.seekTo(_controller.value.position + Duration(seconds: 5)); - setState(() { - // Resume player and update overlay if didn't reached the end - if (!_isEnd) { - _controller.play(); - } - }); - } - - /// Replay action - void _onReplay() { - setState(() { - _controller.play(); - }); - } - - /// Mute / Un-mute action - /// * Switches [_controller] volume from 0 (mute) to 1 (un-mute) - void _onMute() { - final double volume = _mute ? 1.0 : 0.0; - setState(() { - _controller.setVolume(volume); - _mute = !_mute; - }); - } - - /// Update [_progress] when time changed - void _onVideoTimeChanged(double value) { - setState(() { - _progress = value * 0.01; - }); - } - - /// Pauses video player [_controller] while changing time - void _onVideoTimeChangeStart(double value) { - setState(() { - _controller.pause(); - }); - } - - /// Update video player [_controller] when time changed - Future _onVideoTimeChangeEnd(double value) async { - // Parse slider time - final double newValue = max(0, min(value, 99)) * 0.01; - final int millis = (_controller.value.duration.inMilliseconds * newValue).toInt(); - // Change time - await _controller.seekTo(Duration(milliseconds: millis)); - // Resume player - setState(() { - _controller.play(); - }); - } - - /// Get parsed video duration text. - /// * If paused: returns video duration. - /// * If playing: returns video remaining time. - String get _durationText { - late final Duration duration; - if (!_controller.value.isPlaying) { - duration = _controller.value.duration; - } else { - duration = _controller.value.duration - _controller.value.position; - } - int hours = duration.inHours; - int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; - return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; - } - - @override - Widget build(BuildContext context) { - if (!_controller.value.isInitialized || _controller.value.hasError) { - return _thumbnail; - } - return LayoutBuilder(builder: (context, constraints) { - return Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - FittedBox( - fit: BoxFit.contain, - child: SizedBox( - width: _controller.value.size.width, - height: _controller.value.size.height, - child: AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: VideoPlayer(_controller), - ), - ), - ), - _overlay, - ], - ); - }); - } - - /// Video player overlay - Widget get _overlay { - return IgnorePointer( - ignoring: !widget.showOverlay, - child: AnimatedOpacity( - duration: _overlayAnimationDuration, - curve: _overlayAnimationCurve, - opacity: widget.showOverlay ? 1 : 0, - child: Stack( - alignment: Alignment.center, - children: [ - _overlayBackground, - _overlayCenter, - _overlayBottom, - ], - ), - ), - ); - } - - /// Video player center overlay - /// * Fast rewind - /// * Play / Pause / Replay - /// * Fast forward - Widget get _overlayCenter { - // Check if the player is processing / loading. - if ((_controller.value.isBuffering && !_isEnd) || (_isEnd && _controller.value.isPlaying)) { - return Center(child: CircularProgressIndicator()); - } - // If player has ended, show the replay button - if (_isEnd && !_controller.value.isPlaying) { - return Center( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _onReplay, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.replay, - size: 48, - color: Colors.white, - shadows: AppShadows.icon, - ), - ), - ), - ); - } - // Video player is playing - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _onFastRewind, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.fast_rewind, - size: 40, - color: Colors.white, - shadows: AppShadows.icon, - ), - ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _onPlayPause, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - size: 64, - color: Colors.white, - shadows: AppShadows.icon, - ), - ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _onFastForward, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.fast_forward, - size: 40, - color: Colors.white, - shadows: AppShadows.icon, - ), - ), - ), - ], - ); - } - - /// Video player bottom overlay - /// * Duration - /// * Timeline - /// * Mute / Un-mute - Widget get _overlayBottom { - return Positioned( - bottom: widget.screenPadding.bottom, - right: 0, - left: 0, - child: AnimatedSlide( - duration: _overlayAnimationDuration, - curve: _overlayAnimationCurve, - offset: widget.showOverlay ? Offset.zero : Offset(0, 1), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _durationText, - style: TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500), - ), - ), - Expanded( - child: Theme( - // Light slider theme - data: lightTheme, - child: Slider( - value: _isEnd ? 100 : max(0, min(_progress * 100, 100)), - min: 0, - max: 100, - onChanged: _onVideoTimeChanged, - onChangeStart: _onVideoTimeChangeStart, - onChangeEnd: _onVideoTimeChangeEnd, - ), - ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _onMute, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - _mute ? Icons.volume_off : Icons.volume_up, - color: Colors.white, - ), - ), - ), - ], - ), - ), - ), - ); - } - - /// Video player overlay's background color - Widget get _overlayBackground { - return Positioned.fill( - child: Container( - color: Colors.black.withValues(alpha: 0.5), - ), - ); - } - - /// Video thumbnail shown when loading - Widget get _thumbnail { - // Thumbnail doesn't exist - if (widget.thumbnailUrl == null) { - return Center( - child: Icon(Icons.image_not_supported), - ); - } - return Stack( - children: [ - // Thumbnail - Positioned.fill( - child: Image.network( - widget.thumbnailUrl!, - fit: BoxFit.contain, - errorBuilder: (context, o, s) { - debugPrint("$o"); - return Center(child: Icon(Icons.image_not_supported)); - }, - ), - ), - // Loading... - if (!_controller.value.isInitialized) Center(child: CircularProgressIndicator()), - // Error while loading the video - if (_controller.value.hasError) - Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: Colors.black.withValues(alpha: 0.5), - ), - child: Text( - appStrings.videoLoadError_message, - style: TextStyle(fontSize: 16, color: Colors.white), - ), - ), - ), - ], - ); - } -} - -class VideoPlayerView extends StatefulWidget { - const VideoPlayerView( - {Key? key, - this.videoUrl, - this.thumbnailUrl, - this.onToggleOverlay, - this.showOverlay = false, - this.screenPadding = EdgeInsets.zero}) - : super(key: key); - - final String? videoUrl; - final String? thumbnailUrl; - final Function(bool)? onToggleOverlay; - final bool showOverlay; - final EdgeInsets screenPadding; - - @override - State createState() => _VideoPlayerViewState(); -} - -class _VideoPlayerViewState extends State { - late VideoPlayerController _videoPlayerController; - late PlayerProvider _provider; - ChewieController? _chewieController; - - @override - void initState() { - super.initState(); - initializePlayer(); - _provider = PlayerProvider.init(widget.showOverlay); - } - - @override - void dispose() { - _videoPlayerController.dispose(); - _chewieController?.dispose(); - super.dispose(); - } - - Future initializePlayer() async { - if (widget.videoUrl == null) return; - _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl!)); - await _videoPlayerController.initialize(); - _createChewieController(); - setState(() {}); - } - - void _createChewieController() { - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController, - autoPlay: false, - allowFullScreen: false, - showOptions: false, - hideControlsTimer: const Duration(seconds: 3), - customControls: PlayerControls(onToggleOverlay: widget.onToggleOverlay), - )..setVolume(0); - } - - @override - Widget build(BuildContext context) { - if (widget.videoUrl == null) { - return Center( - child: Text(appStrings.errorHUD_label), - ); - } - if (_chewieController == null || !_chewieController!.videoPlayerController.value.isInitialized) { - return Center( - child: CircularProgressIndicator(), - ); - } - return ChangeNotifierProvider.value( - value: _provider, - child: Chewie( - controller: _chewieController!, - ), - ); - } -} diff --git a/lib/views/image/vlc_video_player.dart b/lib/views/image/vlc_video_player.dart new file mode 100644 index 0000000..288d2a5 --- /dev/null +++ b/lib/views/image/vlc_video_player.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_vlc_player_16kb/flutter_vlc_player.dart'; + +class VlcVideoPlayer extends StatefulWidget { + const VlcVideoPlayer({ + super.key, + required this.videoUrl, + required this.thumbnailUrl, + }); + final String videoUrl; + final String? thumbnailUrl; + + static const String routeName = '/image/video_player'; + + @override + State createState() => VlcVideoPlayerState(); +} + +class VlcVideoPlayerState extends State { + late VlcPlayerController vlcController; + late Duration videoDuration; + late Duration videoPosition; + double opacityLevel = 0.0; // Start with controls faded out + + @override + void initState() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + super.initState(); + print(widget.videoUrl); + vlcController = VlcPlayerController.network( + widget.videoUrl, + hwAcc: HwAcc.auto, + autoPlay: true, + options: VlcPlayerOptions(), + ); + videoDuration = Duration(seconds: 0); // Pre init duration is null + videoPosition = Duration(seconds: 0); + vlcController.addListener(() { + // Only update if it's visible + if (vlcController.value.isInitialized || opacityLevel != 0) { + setState(() { + videoDuration = vlcController.value.duration; + videoPosition = vlcController.value.position; + }); + } + }); + } + + void _changeOpacity() { + setState(() => opacityLevel = opacityLevel == 0.0 ? 1.0 : 0.0); + } + + // Convert a Duration to h:mm:ss / mm:ss String format + String formatDuration(Duration d) { + var prettyDuration = (d.inHours > 1 ? "${d.inHours}:" : ''); + prettyDuration += "${d.inMinutes % 60 < 10 ? "0" : ''}${d.inMinutes % 60}:"; + prettyDuration += "${d.inSeconds % 60 < 10 ? "0" : ''}${d.inSeconds % 60}"; + return prettyDuration; + } + + Future _mediaAction() async { + switch (vlcController.value.playingState) { + case PlayingState.stopped || PlayingState.ended: + await vlcController.stop().then((_) => vlcController.play()); + _changeOpacity(); + case PlayingState.paused: + await vlcController.play(); + _changeOpacity(); + case PlayingState.playing || PlayingState.buffering: + await vlcController.pause(); + setState(() {}); + case _: // handle all other cases + {} + } + } + + @override + Future dispose() async { + // Reset fullscreen mode + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + super.dispose(); + // Also dispose VLC controller + await vlcController.stopRendererScanning(); + await vlcController.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final vlcValue = vlcController.value; + // set aspect ratio depending on the screen if VLC doesn't get it right + final vlcAspectRatio = vlcValue.aspectRatio == 1 + ? screenSize.width / screenSize.height + : vlcValue.aspectRatio; + + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + // player + Positioned.fill( + child: Center( + child: AspectRatio( + aspectRatio: vlcAspectRatio, + child: VlcPlayer( + controller: vlcController, + placeholder: const Center(child: CircularProgressIndicator()), + aspectRatio: vlcAspectRatio, + ), + ), + ), + ), + + // Detect any tap to show or hide controls + Positioned.fill(child: GestureDetector(onTap: _changeOpacity)), + + // controllers + IgnorePointer( + // Ignore input when not visible + ignoring: opacityLevel == 0.0, + child: AnimatedOpacity( + opacity: opacityLevel, + duration: Duration(milliseconds: 250), // Magic number :( + child: Column( + children: [ + Padding( + padding: EdgeInsetsGeometry.only(top: 8), + child: Align( + alignment: Alignment.topLeft, + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.arrow_back, color: Colors.white), + ), + ), + ), + Expanded( // Media button + child: Center( + child: IconButton( + onPressed: _mediaAction, + icon: Icon( + switch (vlcController.value.playingState) { + PlayingState.playing => Icons.pause, + PlayingState.paused => Icons.play_arrow, + PlayingState.stopped => Icons.replay, + PlayingState.error => Icons.error, + _ => Icons.play_arrow, + }, + size: 85, // Magic number :( + color: Colors.white, + ), + ), + ), + ), + Row( // Bottom controls and seekbar + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: _mediaAction, + icon: Icon(switch (vlcController.value.playingState) { + PlayingState.playing => Icons.pause, + PlayingState.paused => Icons.play_arrow, + PlayingState.stopped => Icons.replay, + PlayingState.error => Icons.error, + _ => Icons.play_arrow, + }, color: Colors.white), + ), + Text( // May cause issue if video is hour long + formatDuration(videoPosition), + style: TextStyle(color: Colors.white), + ), + Expanded( // Seekbar + child: Slider( + value: videoPosition.inSeconds.toDouble(), + max: videoDuration.inSeconds.toDouble(), + onChanged: (nv) { + setState(() { //convert to Milliseconds since VLC requires ms + vlcController.setTime( + nv.toInt() * Duration.millisecondsPerSecond, + ); + }); + }, + ), + ), + Text( // May cause issue if video is hour long + formatDuration(videoDuration), + style: TextStyle(color: Colors.white), + ), + IconButton( // volume on/off + onPressed: () { + vlcController.setVolume( + vlcController.value.volume == 0 ? 100 : 0, + ); + }, + icon: Icon( + vlcController.value.volume != 0 + ? Icons.volume_up + : Icons.volume_off, + color: Colors.white, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/upload/upload_page.dart b/lib/views/upload/upload_page.dart index afae679..5d6dc45 100644 --- a/lib/views/upload/upload_page.dart +++ b/lib/views/upload/upload_page.dart @@ -22,7 +22,6 @@ import 'package:piwigo_ng/utils/image_actions.dart'; import 'package:piwigo_ng/utils/localizations.dart'; import 'package:piwigo_ng/utils/resources.dart'; import 'package:rounded_loading_button/rounded_loading_button.dart'; -import 'package:video_player/video_player.dart'; class UploadPage extends StatefulWidget { const UploadPage({Key? key, required this.imageList, this.albumId}) : super(key: key); @@ -396,43 +395,21 @@ class VideoUploadItem extends StatefulWidget { } class _VideoUploadItemState extends State { - late VideoPlayerController _controller; + late Image thumbnail; @override void initState() { super.initState(); - _controller = VideoPlayerController.file( - File(widget.path), - videoPlayerOptions: VideoPlayerOptions(), - )..initialize().then((_) => setState(() {})); + } @override void dispose() { - _controller.dispose(); super.dispose(); } - String get _duration { - final Duration duration = _controller.value.duration; - int hours = duration.inHours; - int minutes = (duration - Duration(hours: hours)).inMinutes; - int seconds = (duration - Duration(hours: hours) - Duration(minutes: minutes)).inSeconds; - return '${hours > 0 ? '$hours:' : ''}${minutes < 10 ? '0$minutes' : '$minutes'}:${seconds < 10 ? '0$seconds' : '$seconds'}'; - } - @override Widget build(BuildContext context) { - if (_controller.value.hasError) { - return Center( - child: Icon(Icons.image_not_supported), - ); - } - if (!_controller.value.isInitialized) { - return Center( - child: CircularProgressIndicator(), - ); - } return LayoutBuilder(builder: (context, constraints) { return Stack( alignment: Alignment.center, @@ -442,12 +419,9 @@ class _VideoUploadItemState extends State { child: FittedBox( fit: BoxFit.cover, child: SizedBox( - width: _controller.value.size.width, - height: _controller.value.size.height, - child: AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: VideoPlayer(_controller), - ), + width: thumbnail.width, + height: thumbnail.height, + child: thumbnail, ), ), ), @@ -459,7 +433,7 @@ class _VideoUploadItemState extends State { decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), color: AppColors.black.withValues(alpha: 0.7)), child: Text( - _duration, + "_duration", style: TextStyle(color: AppColors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), diff --git a/pubspec.lock b/pubspec.lock index 4772dab..1000b8c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,14 +97,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" - url: "https://pub.dev" - source: hosted - version: "1.13.0" clock: dependency: transitive description: @@ -169,14 +161,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -536,6 +520,30 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_video_thumbnail_plus: + dependency: "direct main" + description: + name: flutter_video_thumbnail_plus + sha256: "0bf98547192483d0a61c57ec4ac6c5f09b9f50bc3b9982886b0181535a78cf9c" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_vlc_player_16kb: + dependency: "direct main" + description: + name: flutter_vlc_player_16kb + sha256: "69f44e648ad2c1b551d32be452b8e75d54169c9d7f58e23a853964f7e2aee5c5" + url: "https://pub.dev" + source: hosted + version: "7.4.7" + flutter_vlc_player_platform_interface: + dependency: transitive + description: + name: flutter_vlc_player_platform_interface + sha256: "99fbb806f86ce1c53ba007157c183104a100aa643eda0cec7e71118bf0460578" + url: "https://pub.dev" + source: hosted + version: "2.0.5" flutter_web_plugins: dependency: transitive description: flutter @@ -565,14 +573,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" html_unescape: dependency: "direct main" description: @@ -1058,10 +1058,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f" + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.13" shared_preferences_foundation: dependency: transitive description: @@ -1247,10 +1247,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b + sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b" url: "https://pub.dev" source: hosted - version: "6.3.23" + version: "6.3.22" url_launcher_ios: dependency: transitive description: @@ -1315,46 +1315,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" - url: "https://pub.dev" - source: hosted - version: "2.10.0" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "6cfe0b1e102522eda1e139b82bf00602181c5844fd2885340f595fb213d74842" - url: "https://pub.dev" - source: hosted - version: "2.8.14" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd - url: "https://pub.dev" - source: hosted - version: "2.8.4" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a - url: "https://pub.dev" - source: hosted - version: "6.4.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" - source: hosted - version: "2.4.0" vm_service: dependency: transitive description: @@ -1363,30 +1323,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" - wakelock_plus: - dependency: transitive - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" watcher: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.1.3" web: dependency: transitive description: @@ -1407,10 +1351,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "21507ea5a326ceeba4d29dea19e37d92d53d9959cfc746317b9f9f7a57418d87" + sha256: "3c4eb4fcc252b40c2b5ce7be20d0481428b70f3ff589b0a8b8aaeb64c6bed701" url: "https://pub.dev" source: hosted - version: "4.10.3" + version: "4.10.2" webview_flutter_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5cce3e7..8c21cb7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,8 +56,6 @@ dependencies: # Utils mime_type: ^1.0.0 # Check mime type of files (differentiate photos from videos) - video_player: ^2.10.0 # Read video files in fullscreen mode - chewie: ^1.8.1 # Video player with options flutter_image_compress: ^2.4.0 # Remove metadata permission_handler: ^12.0.0+1 # Check and asks for permissions share_plus: ^12.0.0 # Share files @@ -65,6 +63,10 @@ dependencies: provider: ^6.0.3 # Notifiers for theme and language changes listen_sharing_intent: ^1.9.2 # Receive sharing from other apps + # Video player + flutter_vlc_player_16kb: ^7.4.6 # LibVLC player + flutter_video_thumbnail_plus: ^1.0.1 # Generate thumbnail + # Translations flutter_localizations: sdk: flutter