mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor: simplify video zooming (#26527)
fix: simplify video zooming # Conflicts: # mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart # mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -14,15 +14,15 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.wi
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
|
||||||
@@ -53,7 +53,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
||||||
final ValueNotifier<PhotoViewScaleState> _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial);
|
|
||||||
|
|
||||||
double _snapOffset = 0.0;
|
double _snapOffset = 0.0;
|
||||||
|
|
||||||
@@ -79,7 +78,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_proxyScrollController.dispose();
|
_proxyScrollController.dispose();
|
||||||
_scaleBoundarySub?.cancel();
|
_scaleBoundarySub?.cancel();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
_videoScaleStateNotifier.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,17 +247,14 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
||||||
|
|
||||||
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
||||||
_isZoomed =
|
_isZoomed = switch (scaleState) {
|
||||||
scaleState == PhotoViewScaleState.zoomedIn ||
|
PhotoViewScaleState.zoomedIn || PhotoViewScaleState.covering => true,
|
||||||
scaleState == PhotoViewScaleState.covering ||
|
_ => false,
|
||||||
_videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn ||
|
};
|
||||||
_videoScaleStateNotifier.value == PhotoViewScaleState.covering;
|
|
||||||
_viewer.setZoomed(_isZoomed);
|
_viewer.setZoomed(_isZoomed);
|
||||||
|
|
||||||
if (scaleState != PhotoViewScaleState.initial) {
|
if (scaleState != PhotoViewScaleState.initial) {
|
||||||
if (_dragStart == null) _viewer.setControls(false);
|
if (_dragStart == null) _viewer.setControls(false);
|
||||||
|
|
||||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,35 +329,40 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhotoView.customChild(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
PhotoView.customChild(
|
||||||
key: Key(displayAsset.heroTag),
|
key: Key(displayAsset.heroTag),
|
||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
onDragCancel: _onDragCancel,
|
onDragCancel: _onDragCancel,
|
||||||
|
onTapUp: _onTapUp,
|
||||||
heroAttributes: heroAttributes,
|
heroAttributes: heroAttributes,
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
basePosition: Alignment.center,
|
basePosition: Alignment.center,
|
||||||
disableScaleGestures: true,
|
disableScaleGestures: showingDetails,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
scaleStateChangedCallback: _onScaleStateChanged,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
|
||||||
tightMode: true,
|
|
||||||
onPageBuild: _onPageBuild,
|
onPageBuild: _onPageBuild,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
|
child: SizedBox(
|
||||||
|
width: context.width,
|
||||||
|
height: context.height,
|
||||||
child: NativeVideoViewer(
|
child: NativeVideoViewer(
|
||||||
key: _NativeVideoViewerKey(displayAsset.heroTag),
|
key: _NativeVideoViewerKey(displayAsset.heroTag),
|
||||||
asset: displayAsset,
|
asset: displayAsset,
|
||||||
scaleStateNotifier: _videoScaleStateNotifier,
|
|
||||||
disableScaleGestures: showingDetails,
|
|
||||||
image: Image(
|
image: Image(
|
||||||
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
||||||
|
fit: BoxFit.contain,
|
||||||
height: context.height,
|
height: context.height,
|
||||||
width: context.width,
|
width: context.width,
|
||||||
fit: BoxFit.contain,
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Center(child: VideoViewerControls()),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widge
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
@@ -26,7 +24,6 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:native_video_player/native_video_player.dart';
|
import 'package:native_video_player/native_video_player.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -51,21 +48,10 @@ bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) {
|
|||||||
class NativeVideoViewer extends HookConsumerWidget {
|
class NativeVideoViewer extends HookConsumerWidget {
|
||||||
static final log = Logger('NativeVideoViewer');
|
static final log = Logger('NativeVideoViewer');
|
||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final bool showControls;
|
|
||||||
final int playbackDelayFactor;
|
final int playbackDelayFactor;
|
||||||
final Widget image;
|
final Widget image;
|
||||||
final ValueNotifier<PhotoViewScaleState>? scaleStateNotifier;
|
|
||||||
final bool disableScaleGestures;
|
|
||||||
|
|
||||||
const NativeVideoViewer({
|
const NativeVideoViewer({super.key, required this.asset, required this.image, this.playbackDelayFactor = 1});
|
||||||
super.key,
|
|
||||||
required this.asset,
|
|
||||||
required this.image,
|
|
||||||
this.showControls = true,
|
|
||||||
this.playbackDelayFactor = 1,
|
|
||||||
this.scaleStateNotifier,
|
|
||||||
this.disableScaleGestures = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -144,7 +130,6 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
||||||
final aspectRatio = useState<double?>(null);
|
final aspectRatio = useState<double?>(null);
|
||||||
|
|
||||||
useMemoized(() async {
|
useMemoized(() async {
|
||||||
if (!context.mounted || aspectRatio.value != null) {
|
if (!context.mounted || aspectRatio.value != null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -320,20 +305,6 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||||
}
|
}
|
||||||
|
|
||||||
Size? videoContextSize(double? videoAspectRatio, BuildContext? context) {
|
|
||||||
Size? videoContextSize;
|
|
||||||
if (videoAspectRatio == null || context == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final contextAspectRatio = context.width / context.height;
|
|
||||||
if (videoAspectRatio > contextAspectRatio) {
|
|
||||||
videoContextSize = Size(context.width, context.width / aspectRatio.value!);
|
|
||||||
} else {
|
|
||||||
videoContextSize = Size(context.height * aspectRatio.value!, context.height);
|
|
||||||
}
|
|
||||||
return videoContextSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.listen(currentAssetNotifier, (_, value) {
|
ref.listen(currentAssetNotifier, (_, value) {
|
||||||
final playerController = controller.value;
|
final playerController = controller.value;
|
||||||
if (playerController != null && value != asset) {
|
if (playerController != null && value != asset) {
|
||||||
@@ -414,29 +385,22 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return SizedBox(
|
return Stack(
|
||||||
width: context.width,
|
|
||||||
height: context.height,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
// Hide thumbnail once video is visible to avoid it showing in background when zooming out on video.
|
// This remains under the video to avoid flickering
|
||||||
if (!isVisible.value || controller.value == null) Center(child: image),
|
// For motion videos, this is the image portion of the asset
|
||||||
if (aspectRatio.value != null && !isCasting && isCurrent)
|
Center(child: image),
|
||||||
|
if (aspectRatio.value != null && !isCasting)
|
||||||
Visibility.maintain(
|
Visibility.maintain(
|
||||||
visible: isVisible.value,
|
visible: isVisible.value,
|
||||||
child: PhotoView.customChild(
|
child: Center(
|
||||||
enableRotation: false,
|
child: AspectRatio(
|
||||||
disableScaleGestures: disableScaleGestures,
|
aspectRatio: aspectRatio.value!,
|
||||||
// Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet.
|
child: isCurrent ? NativeVideoPlayerView(onViewReady: initController) : null,
|
||||||
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
|
),
|
||||||
scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state,
|
|
||||||
childSize: videoContextSize(aspectRatio.value, context),
|
|
||||||
child: NativeVideoPlayerView(onViewReady: initController),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showControls) const Center(child: VideoViewerControls()),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class DriftMemoryCard extends StatelessWidget {
|
|||||||
child: NativeVideoViewer(
|
child: NativeVideoViewer(
|
||||||
key: ValueKey(asset.id),
|
key: ValueKey(asset.id),
|
||||||
asset: asset,
|
asset: asset,
|
||||||
showControls: false,
|
|
||||||
playbackDelayFactor: 2,
|
playbackDelayFactor: 2,
|
||||||
image: FullImage(asset, size: Size(context.width, context.height), fit: BoxFit.contain),
|
image: FullImage(asset, size: Size(context.width, context.height), fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user