From 654f6c0a4329bbb3986b91be0d994bfa0ab95db2 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 8 Jun 2024 01:33:27 +0800 Subject: [PATCH] Support playing live photos taken with a Google Pixel --- .../2.0x/ic_motion_photos_play_24dp.png | Bin 0 -> 1503 bytes .../3.0x/ic_motion_photos_play_24dp.png | Bin 0 -> 2313 bytes app/assets/ic_motion_photos_play_24dp.png | Bin 0 -> 794 bytes app/lib/asset.dart | 1 + app/lib/live_photo_util.dart | 13 + app/lib/widget/horizontal_page_viewer.dart | 2 +- app/lib/widget/live_photo_viewer.dart | 290 ++++++++++++++++++ app/lib/widget/live_photo_viewer.g.dart | 14 + app/lib/widget/page_changed_listener.dart | 4 +- app/lib/widget/png_icon.dart | 17 + app/lib/widget/video_viewer.dart | 4 +- app/lib/widget/viewer.dart | 63 +++- app/pubspec.lock | 29 +- app/pubspec.yaml | 12 +- 14 files changed, 425 insertions(+), 24 deletions(-) create mode 100644 app/assets/2.0x/ic_motion_photos_play_24dp.png create mode 100644 app/assets/3.0x/ic_motion_photos_play_24dp.png create mode 100644 app/assets/ic_motion_photos_play_24dp.png create mode 100644 app/lib/live_photo_util.dart create mode 100644 app/lib/widget/live_photo_viewer.dart create mode 100644 app/lib/widget/live_photo_viewer.g.dart create mode 100644 app/lib/widget/png_icon.dart diff --git a/app/assets/2.0x/ic_motion_photos_play_24dp.png b/app/assets/2.0x/ic_motion_photos_play_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ca44a7ab79e1922469b8fb62b63c5b5ca6142644 GIT binary patch literal 1503 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 literal 0 HcmV?d00001 diff --git a/app/assets/ic_motion_photos_play_24dp.png b/app/assets/ic_motion_photos_play_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7a84c82f8b88113f5c37bdf27888d711ac713e5 GIT binary patch literal 794 zcmV+#1LgdQP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10;fqt zK~zY`&6Z6|RACgyf9K9<_OfxtnNiFLa#2!H81x0gz#vDWeQ(r8v5hX;=%zy5MFa&> zpTM_;8|ecS+NOlailk`neeT>lP?D%K_q6C1>f9Mk5PIO@KAiLLKj-|z*tzCS-D=Rt9`J(H<_Q>KgQ1%N>0Re1y=Q{nL@5R1TG z5s};JRN`7wQ`64~eM|$chmUKz?4d2F*4ifj!8V@eOupyeADY(}wwU*-TJuM~shb+( zb*soY${MPjnN0G_C0cdh z7~^GCtWnU`Z5Uc4(Edy&IklUKR%5$? onPageChanged; + final void Function(int from, int to)? onPageChanged; } class _HorizontalPageViewerState extends State { diff --git a/app/lib/widget/live_photo_viewer.dart b/app/lib/widget/live_photo_viewer.dart new file mode 100644 index 00000000..4c1475df --- /dev/null +++ b/app/lib/widget/live_photo_viewer.dart @@ -0,0 +1,290 @@ +import 'dart:async'; + +import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/api/api_util.dart' as api_util; +import 'package:nc_photos/cache_manager_util.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/np_api_util.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/use_case/request_public_link.dart'; +import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_platform_util/np_platform_util.dart'; +import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +part 'live_photo_viewer.g.dart'; + +class LivePhotoViewer extends StatefulWidget { + const LivePhotoViewer({ + super.key, + required this.account, + required this.file, + this.onLoaded, + this.onLoadFailure, + this.onHeightChanged, + this.canPlay = true, + this.livePhotoType, + }); + + @override + State createState() => _LivePhotoViewerState(); + + final Account account; + final FileDescriptor file; + final VoidCallback? onLoaded; + final VoidCallback? onLoadFailure; + final ValueChanged? onHeightChanged; + final bool canPlay; + final LivePhotoType? livePhotoType; +} + +@npLog +class _LivePhotoViewerState extends State { + @override + void initState() { + super.initState(); + _getVideoUrl().then((url) { + if (mounted) { + _initController(url); + } + }).onError((e, stacktrace) { + _log.shout("[initState] Failed while _getVideoUrl", e, stacktrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + widget.onLoadFailure?.call(); + }); + + _lifecycleListener = AppLifecycleListener(onShow: () { + if (_controller.value.isInitialized) { + _controller.pause(); + } + }); + } + + @override + void dispose() { + _lifecycleListener.dispose(); + _controller.removeListener(_onControllerChanged); + _controllerValue?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget content; + if (_isControllerInitialized && _controller.value.isInitialized) { + content = _buildPlayer(context); + } else { + content = _PlaceHolderView( + account: widget.account, + file: widget.file, + ); + } + + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + alignment: Alignment.center, + child: content, + ); + } + + Future _initController(String url) async { + try { + _controllerValue = VideoPlayerController.networkUrl( + Uri.parse(url), + httpHeaders: { + "Authorization": AuthUtil.fromAccount(widget.account).toHeaderValue(), + }, + livePhotoType: widget.livePhotoType, + ); + await _controller.initialize(); + await _controller.setVolume(0); + await _controller.setLooping(true); + widget.onLoaded?.call(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_key.currentContext != null) { + widget.onHeightChanged?.call(_key.currentContext!.size!.height); + } + }); + _controller.addListener(_onControllerChanged); + setState(() { + _isControllerInitialized = true; + }); + await _controller.play(); + } catch (e, stackTrace) { + _log.shout("[_initController] Failed while initialize", e, stackTrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + widget.onLoadFailure?.call(); + } + } + + Widget _buildPlayer(BuildContext context) { + if (_controller.value.isPlaying && !widget.canPlay) { + _log.info("Pause playback"); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.pause(); + }); + } else if (!_controller.value.isPlaying && widget.canPlay) { + _log.info("Resume playback"); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.play(); + }); + } + return Center( + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: AspectRatio( + key: _key, + aspectRatio: _controller.value.aspectRatio, + child: IgnorePointer( + child: VideoPlayer(_controller), + ), + ), + ), + if (!_isLoaded) ...[ + _PlaceHolderView( + account: widget.account, + file: widget.file, + ), + ], + ], + ), + ); + } + + Future _getVideoUrl() async { + if (getRawPlatform() == NpPlatform.web) { + return RequestPublicLink()(widget.account, widget.file); + } else { + return api_util.getFileUrl(widget.account, widget.file); + } + } + + void _onControllerChanged() { + if (!_controller.value.isInitialized) { + return; + } + if (_controller.value.isPlaying != _isPlaying) { + setState(() { + _isPlaying = !_isPlaying; + _isLoaded = true; + }); + } + } + + VideoPlayerController get _controller => _controllerValue!; + + final _key = GlobalKey(); + bool _isControllerInitialized = false; + VideoPlayerController? _controllerValue; + var _isPlaying = false; + var _isLoaded = false; + late final AppLifecycleListener _lifecycleListener; +} + +class _PlaceHolderView extends StatelessWidget { + const _PlaceHolderView({ + required this.account, + required this.file, + }); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + mod.CachedNetworkImage( + fit: BoxFit.contain, + cacheManager: LargeImageCacheManager.inst, + imageUrl: api_util.getFilePreviewUrl( + account, + file, + width: k.photoLargeSize, + height: k.photoLargeSize, + isKeepAspectRatio: true, + ), + httpHeaders: { + "Authorization": AuthUtil.fromAccount(account).toHeaderValue(), + }, + fadeInDuration: const Duration(), + filterQuality: FilterQuality.high, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + imageBuilder: (context, child, imageProvider) { + const SizeChangedLayoutNotification().dispatch(context); + return child; + }, + ), + ColoredBox(color: Colors.black.withOpacity(.7)), + const Center(child: _ProgressIndicator()), + ], + ); + } + + final Account account; + final FileDescriptor file; +} + +class _ProgressIndicator extends StatefulWidget { + const _ProgressIndicator(); + + @override + State createState() => _ProgressIndicatorState(); +} + +class _ProgressIndicatorState extends State<_ProgressIndicator> + with TickerProviderStateMixin { + @override + void initState() { + super.initState(); + animationController.repeat(); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + RotationTransition( + turns: animationController + .drive(CurveTween(curve: Curves.easeInOutCubic)), + filterQuality: FilterQuality.high, + child: Icon( + Icons.motion_photos_on_outlined, + size: 64, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Icon( + Icons.play_arrow_rounded, + size: 48, + color: Theme.of(context).colorScheme.primary, + ), + ], + ); + } + + late final animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); +} diff --git a/app/lib/widget/live_photo_viewer.g.dart b/app/lib/widget/live_photo_viewer.g.dart new file mode 100644 index 00000000..44f0272e --- /dev/null +++ b/app/lib/widget/live_photo_viewer.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'live_photo_viewer.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_LivePhotoViewerStateNpLog on _LivePhotoViewerState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.live_photo_viewer._LivePhotoViewerState"); +} diff --git a/app/lib/widget/page_changed_listener.dart b/app/lib/widget/page_changed_listener.dart index f3aeeac5..9e1084d3 100644 --- a/app/lib/widget/page_changed_listener.dart +++ b/app/lib/widget/page_changed_listener.dart @@ -10,13 +10,13 @@ class PageChangedListener { if (pageController.hasClients) { final page = pageController.page!.round(); if (page != _prevPage) { - onPageChanged?.call(page); + onPageChanged?.call(_prevPage, page); _prevPage = page; } } } final PageController pageController; - final ValueChanged? onPageChanged; + final void Function(int from, int to)? onPageChanged; int _prevPage; } diff --git a/app/lib/widget/png_icon.dart b/app/lib/widget/png_icon.dart new file mode 100644 index 00000000..2215a5c2 --- /dev/null +++ b/app/lib/widget/png_icon.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PngIcon extends StatelessWidget { + const PngIcon( + this.asset, { + super.key, + this.size = 24, + }); + + @override + Widget build(BuildContext context) { + return Image.asset(asset, width: size, height: size); + } + + final String asset; + final double size; +} diff --git a/app/lib/widget/video_viewer.dart b/app/lib/widget/video_viewer.dart index 9ed13a9b..6ae10867 100644 --- a/app/lib/widget/video_viewer.dart +++ b/app/lib/widget/video_viewer.dart @@ -110,8 +110,8 @@ class _VideoViewerState extends State Future _initController(String url) async { try { - _controllerValue = VideoPlayerController.network( - url, + _controllerValue = VideoPlayerController.networkUrl( + Uri.parse(url), httpHeaders: { "Authorization": AuthUtil.fromAccount(widget.account).toHeaderValue(), }, diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 58f06c48..511970f8 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -11,6 +11,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/asset.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/collection_items_controller.dart'; import 'package:nc_photos/di_container.dart'; @@ -23,6 +24,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/flutter_util.dart'; import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/live_photo_util.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/share_handler.dart'; @@ -34,6 +36,8 @@ import 'package:nc_photos/widget/horizontal_page_viewer.dart'; import 'package:nc_photos/widget/image_editor.dart'; import 'package:nc_photos/widget/image_enhancer.dart'; import 'package:nc_photos/widget/image_viewer.dart'; +import 'package:nc_photos/widget/live_photo_viewer.dart'; +import 'package:nc_photos/widget/png_icon.dart'; import 'package:nc_photos/widget/slideshow_dialog.dart'; import 'package:nc_photos/widget/slideshow_viewer.dart'; import 'package:nc_photos/widget/video_viewer.dart'; @@ -43,6 +47,7 @@ import 'package:nc_photos/widget/viewer_mixin.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/or_null.dart'; import 'package:np_platform_util/np_platform_util.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; part 'viewer.g.dart'; @@ -164,8 +169,10 @@ class _ViewerState extends State controller: _viewerController, viewportFraction: _viewportFraction, canSwitchPage: _canSwitchPage(), - onPageChanged: (_) { - setState(() {}); + onPageChanged: (from, to) { + setState(() { + _pageStates[from]?.shouldPlayLivePhoto = false; + }); }, ), if (_isShowAppBar) @@ -206,6 +213,17 @@ class _ViewerState extends State centerTitle: isCentered, actions: [ if (!_isDetailPaneActive && _canOpenDetailPane()) ...[ + if (getLivePhotoTypeFromFile(file) != null) + if (_pageStates[index]?.shouldPlayLivePhoto ?? false) + IconButton( + icon: const Icon(Icons.motion_photos_pause_outlined), + onPressed: () => _onPauseMotionPhotosPressed(index), + ) + else + IconButton( + icon: const PngIcon(icMotionPhotosPlay24dp), + onPressed: () => _onPlayMotionPhotosPressed(index), + ), (_pageStates[index]?.favoriteOverride ?? file.fdIsFavorite) == true ? IconButton( icon: const Icon(Icons.star), @@ -361,6 +379,15 @@ class _ViewerState extends State Widget _buildItemView(BuildContext context, int index) { final file = _streamFilesView[index]; if (file_util.isSupportedImageFormat(file)) { + final shouldPlayLivePhoto = _pageStates[index]!.shouldPlayLivePhoto; + if (shouldPlayLivePhoto) { + final livePhotoType = getLivePhotoTypeFromFile(file); + if (livePhotoType != null) { + return _buildLivePhotoView(context, index, livePhotoType); + } else { + _log.warning("[_buildItemView] Not a live photo"); + } + } return _buildImageView(context, index); } else if (file_util.isSupportedVideoFormat(file)) { return _buildVideoView(context, index); @@ -404,6 +431,25 @@ class _ViewerState extends State ); } + Widget _buildLivePhotoView( + BuildContext context, int index, LivePhotoType livePhotoType) { + return LivePhotoViewer( + account: widget.account, + file: _streamFilesView[index], + onLoaded: () => _onVideoLoaded(index), + onHeightChanged: (height) => _updateItemHeight(index, height), + canPlay: !_isDetailPaneActive, + livePhotoType: livePhotoType, + onLoadFailure: () { + if (mounted) { + setState(() { + _pageStates[index]!.shouldPlayLivePhoto = false; + }); + } + }, + ); + } + bool _onPageContentScrolled(ScrollNotification notification, int index) { if (!_canOpenDetailPane()) { return false; @@ -538,6 +584,18 @@ class _ViewerState extends State } } + void _onPlayMotionPhotosPressed(int index) { + setState(() { + _pageStates[index]!.shouldPlayLivePhoto = true; + }); + } + + void _onPauseMotionPhotosPressed(int index) { + setState(() { + _pageStates[index]!.shouldPlayLivePhoto = false; + }); + } + Future _onFavoritePressed(int index) async { if (_pageStates[index]!.isProcessingFavorite) { _log.fine("[_onFavoritePressed] Process ongoing, ignored"); @@ -931,6 +989,7 @@ class _PageState { bool isProcessingFavorite = false; bool? favoriteOverride; + bool shouldPlayLivePhoto = false; } class _AppBarTitle extends StatelessWidget { diff --git a/app/pubspec.lock b/app/pubspec.lock index 26a33005..45510a7e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1776,36 +1776,37 @@ packages: dependency: "direct main" description: path: "packages/video_player/video_player" - ref: "video_player-v2.4.5-nc-photos-2" - resolved-ref: b5c28c21f29f09b623900d5a8cc88a70da29de3a + ref: "video_player-v2.8.6-nc-photos-1" + resolved-ref: ea754fd61b8bb3c431bd33d1a07709b6f501345c url: "https://gitlab.com/nc-photos/flutter-plugins" source: git - version: "2.4.5" + version: "2.8.6" video_player_android: dependency: "direct overridden" description: path: "packages/video_player/video_player_android" - ref: "video_player-v2.4.5-nc-photos-2" - resolved-ref: b5c28c21f29f09b623900d5a8cc88a70da29de3a + ref: "video_player-v2.8.6-nc-photos-1" + resolved-ref: ea754fd61b8bb3c431bd33d1a07709b6f501345c url: "https://gitlab.com/nc-photos/flutter-plugins" source: git - version: "2.3.6" + version: "2.4.12" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "90468226c8687adf7b567d9bb42c25588783c4d30509af1fbd663b2dd049f700" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.1" video_player_platform_interface: - dependency: transitive + dependency: "direct main" description: - name: video_player_platform_interface - sha256: "318a6d20577e1c78cf0bf40670883cc571ea860c72a4f7426d7dacce4bdd4343" - url: "https://pub.dev" - source: hosted - version: "5.1.4" + path: "packages/video_player/video_player_platform_interface" + ref: "video_player-v2.8.6-nc-photos-1" + resolved-ref: ea754fd61b8bb3c431bd33d1a07709b6f501345c + url: "https://gitlab.com/nc-photos/flutter-plugins" + source: git + version: "6.2.2" video_player_web: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 67a9147e..88b4c26c 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -153,7 +153,8 @@ dependencies: tuple: ^2.0.2 url_launcher: ^6.2.6 uuid: ^3.0.7 - video_player: 2.4.5 + video_player: + video_player_platform_interface: visibility_detector: ^0.4.0+2 wakelock_plus: ^1.1.1 woozy_search: ^2.0.3 @@ -162,13 +163,18 @@ dependency_overrides: video_player: git: url: https://gitlab.com/nc-photos/flutter-plugins - ref: video_player-v2.4.5-nc-photos-2 + ref: video_player-v2.8.6-nc-photos-1 path: packages/video_player/video_player video_player_android: git: url: https://gitlab.com/nc-photos/flutter-plugins - ref: video_player-v2.4.5-nc-photos-2 + ref: video_player-v2.8.6-nc-photos-1 path: packages/video_player/video_player_android + video_player_platform_interface: + git: + url: https://gitlab.com/nc-photos/flutter-plugins + ref: video_player-v2.8.6-nc-photos-1 + path: packages/video_player/video_player_platform_interface dev_dependencies: test: ^1.22.1