mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
wip separate widget
This commit is contained in:
parent
ba499d9f54
commit
338e5a8e5c
@ -12,7 +12,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/download_panel.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_loader.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
@ -366,8 +366,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
maxScale: 1.0,
|
||||
minScale: 1.0,
|
||||
basePosition: Alignment.center,
|
||||
child: NativeVideoViewerPage(
|
||||
key: ValueKey(a),
|
||||
child: NativeVideoLoader(
|
||||
key: ValueKey(a.id),
|
||||
asset: a,
|
||||
isMotionVideo: a.livePhotoVideoId != null,
|
||||
loopVideo: shouldLoopVideo.value,
|
||||
|
169
mobile/lib/pages/common/native_video_loader.dart
Normal file
169
mobile/lib/pages/common/native_video_loader.dart
Normal file
@ -0,0 +1,169 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class NativeVideoLoader extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final bool isMotionVideo;
|
||||
final Widget placeholder;
|
||||
final bool showControls;
|
||||
final Duration hideControlsTimer;
|
||||
final bool loopVideo;
|
||||
|
||||
const NativeVideoLoader({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.placeholder,
|
||||
this.isMotionVideo = false,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.loopVideo = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// fast path for aspect ratio
|
||||
final initAspectRatio = useMemoized(
|
||||
() {
|
||||
if (asset.exifInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final width = asset.orientatedWidth?.toDouble();
|
||||
final height = asset.orientatedHeight?.toDouble();
|
||||
return width != null && height != null && width > 0 && height > 0
|
||||
? width / height
|
||||
: null;
|
||||
},
|
||||
);
|
||||
|
||||
final localEntity = useMemoized(
|
||||
() => asset.localId != null ? AssetEntity.fromId(asset.localId!) : null,
|
||||
);
|
||||
Future<double> calculateAspectRatio() async {
|
||||
late final double? orientatedWidth;
|
||||
late final double? orientatedHeight;
|
||||
|
||||
if (asset.exifInfo != null) {
|
||||
orientatedWidth = asset.orientatedWidth?.toDouble();
|
||||
orientatedHeight = asset.orientatedHeight?.toDouble();
|
||||
} else if (localEntity != null) {
|
||||
final entity = await localEntity;
|
||||
orientatedWidth = entity?.orientatedWidth.toDouble();
|
||||
orientatedHeight = entity?.orientatedHeight.toDouble();
|
||||
} else {
|
||||
final entity = await ref.read(assetServiceProvider).loadExif(asset);
|
||||
orientatedWidth = entity.orientatedWidth?.toDouble();
|
||||
orientatedHeight = entity.orientatedHeight?.toDouble();
|
||||
}
|
||||
|
||||
if (orientatedWidth != null &&
|
||||
orientatedHeight != null &&
|
||||
orientatedWidth > 0 &&
|
||||
orientatedHeight > 0) {
|
||||
return orientatedWidth / orientatedHeight;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
final aspectRatioFuture = useMemoized(
|
||||
() => initAspectRatio == null ? calculateAspectRatio() : null,
|
||||
);
|
||||
|
||||
final log = Logger('NativeVideoLoader');
|
||||
log.info('Building NativeVideoLoader');
|
||||
|
||||
Future<VideoSource> createLocalSource() async {
|
||||
log.info('Loading video from local storage');
|
||||
final entity = await localEntity;
|
||||
if (entity == null) {
|
||||
throw Exception('No entity found for the video');
|
||||
}
|
||||
|
||||
final file = await entity.file;
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
return await VideoSource.init(
|
||||
path: file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
}
|
||||
|
||||
Future<VideoSource> createRemoteSource() async {
|
||||
log.info('Loading video from server');
|
||||
|
||||
// Use a network URL for the video player controller
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final String videoUrl = asset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
|
||||
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
|
||||
|
||||
return await VideoSource.init(
|
||||
path: videoUrl,
|
||||
type: VideoSourceType.network,
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<VideoSource> createSource(Asset asset) async {
|
||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
||||
return createLocalSource();
|
||||
}
|
||||
|
||||
return createRemoteSource();
|
||||
}
|
||||
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
||||
return SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) => ref
|
||||
.read(videoPlaybackValueProvider.notifier)
|
||||
.value = VideoPlaybackValue.uninitialized(),
|
||||
child: SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: FutureBuilder(
|
||||
key: ValueKey(asset.id),
|
||||
future: aspectRatioFuture,
|
||||
initialData: initAspectRatio,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
return NativeVideoViewerPage(
|
||||
key: ValueKey(asset.id),
|
||||
videoSource: createSource(asset),
|
||||
duration: asset.duration,
|
||||
aspectRatio: snapshot.data as double,
|
||||
isMotionVideo: isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
loopVideo: loopVideo,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,32 +3,47 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_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/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
|
||||
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// bufferingTimer.value.cancel();
|
||||
// try {
|
||||
// controller.value?.onPlaybackPositionChanged
|
||||
// .removeListener(onPlaybackPositionChanged);
|
||||
// controller.value?.onPlaybackStatusChanged
|
||||
// .removeListener(onPlaybackPositionChanged);
|
||||
// controller.value?.onPlaybackReady.removeListener(onPlaybackReady);
|
||||
// controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded);
|
||||
// controller.value?.stop();
|
||||
// } catch (_) {
|
||||
// // Consume error from the controller
|
||||
// }
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final Future<VideoSource> videoSource;
|
||||
final double aspectRatio;
|
||||
final Duration duration;
|
||||
final bool isMotionVideo;
|
||||
final Widget? placeholder;
|
||||
final bool showControls;
|
||||
final Duration hideControlsTimer;
|
||||
final bool loopVideo;
|
||||
|
||||
const NativeVideoViewerPage({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.videoSource,
|
||||
required this.aspectRatio,
|
||||
required this.duration,
|
||||
this.isMotionVideo = false,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.loopVideo = false,
|
||||
@ -36,79 +51,53 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useState<NativeVideoPlayerController?>(null);
|
||||
final controller = useRef<NativeVideoPlayerController?>(null);
|
||||
final lastVideoPosition = useRef(-1);
|
||||
final isBuffering = useRef(false);
|
||||
final width = useRef<double>(asset.width?.toDouble() ?? 1.0);
|
||||
final height = useRef<double>(asset.height?.toDouble() ?? 1.0);
|
||||
|
||||
final log = Logger('NativeVideoViewerPage');
|
||||
log.info('Building NativeVideoViewerPage');
|
||||
|
||||
void checkIfBuffering([Timer? timer]) {
|
||||
if (!context.mounted) {
|
||||
timer?.cancel();
|
||||
return;
|
||||
return timer?.cancel();
|
||||
}
|
||||
|
||||
log.info('Checking if buffering');
|
||||
final videoPlayback = ref.read(videoPlaybackValueProvider);
|
||||
if ((isBuffering.value ||
|
||||
videoPlayback.state == VideoPlaybackState.initializing) &&
|
||||
videoPlayback.state != VideoPlaybackState.buffering) {
|
||||
log.info('Marking video as buffering');
|
||||
ref.read(videoPlaybackValueProvider.notifier).value =
|
||||
videoPlayback.copyWith(state: VideoPlaybackState.buffering);
|
||||
}
|
||||
}
|
||||
|
||||
// timer to mark videos as buffering if the position does not change
|
||||
final bufferingTimer = useRef<Timer>(
|
||||
Timer.periodic(const Duration(seconds: 5), checkIfBuffering),
|
||||
);
|
||||
|
||||
Future<VideoSource> createSource(Asset asset) async {
|
||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
||||
final entity = await AssetEntity.fromId(asset.localId!);
|
||||
final file = await entity?.file;
|
||||
if (entity == null || file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
width.value = entity.orientatedWidth.toDouble();
|
||||
height.value = entity.orientatedHeight.toDouble();
|
||||
|
||||
return await VideoSource.init(
|
||||
path: file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
} else {
|
||||
final assetWithExif =
|
||||
await ref.read(assetServiceProvider).loadExif(asset);
|
||||
|
||||
width.value = assetWithExif.orientatedWidth?.toDouble() ?? width.value;
|
||||
height.value =
|
||||
assetWithExif.orientatedHeight?.toDouble() ?? height.value;
|
||||
|
||||
// Use a network URL for the video player controller
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final String videoUrl = asset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
|
||||
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
|
||||
|
||||
return await VideoSource.init(
|
||||
path: videoUrl,
|
||||
type: VideoSourceType.network,
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
);
|
||||
}
|
||||
}
|
||||
useInterval(const Duration(seconds: 5), checkIfBuffering);
|
||||
|
||||
// When the volume changes, set the volume
|
||||
ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
|
||||
(_, mute) {
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to seek to');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (mute) {
|
||||
controller.value?.setVolume(0.0);
|
||||
log.info('Muting video');
|
||||
playerController.setVolume(0.0);
|
||||
log.info('Muted video');
|
||||
} else {
|
||||
controller.value?.setVolume(0.7);
|
||||
log.info('Unmuting video');
|
||||
playerController.setVolume(0.7);
|
||||
log.info('Unmuted video');
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
log.severe('Error setting volume: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
});
|
||||
@ -116,16 +105,21 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
// When the position changes, seek to the position
|
||||
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
|
||||
(_, position) {
|
||||
if (controller.value == null) {
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to seek to');
|
||||
// No seeeking if there is no video
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the position to seek to
|
||||
final Duration seek = asset.duration * (position / 100.0);
|
||||
final Duration seek = duration * (position / 100.0);
|
||||
try {
|
||||
controller.value?.seekTo(seek.inSeconds);
|
||||
} catch (_) {
|
||||
log.info('Seeking to position: ${seek.inSeconds}');
|
||||
playerController.seekTo(seek.inSeconds);
|
||||
log.info('Seeked to position: ${seek.inSeconds}');
|
||||
} catch (error) {
|
||||
log.severe('Error seeking to position $position: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
});
|
||||
@ -135,108 +129,172 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
(_, pause) {
|
||||
try {
|
||||
if (pause) {
|
||||
log.info('Pausing video');
|
||||
controller.value?.pause();
|
||||
log.info('Paused video');
|
||||
} else {
|
||||
log.info('Playing video');
|
||||
controller.value?.play();
|
||||
log.info('Played video');
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
log.severe('Error pausing or playing video: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
});
|
||||
|
||||
void updateVideoPlayback() {
|
||||
if (controller.value == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final videoPlayback =
|
||||
VideoPlaybackValue.fromNativeController(controller.value!);
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
// Check if the video is buffering
|
||||
if (videoPlayback.state == VideoPlaybackState.playing) {
|
||||
isBuffering.value =
|
||||
lastVideoPosition.value == videoPlayback.position.inSeconds;
|
||||
lastVideoPosition.value = videoPlayback.position.inSeconds;
|
||||
} else {
|
||||
isBuffering.value = false;
|
||||
lastVideoPosition.value = -1;
|
||||
}
|
||||
final state = videoPlayback.state;
|
||||
|
||||
// Enable the WakeLock while the video is playing
|
||||
if (state == VideoPlaybackState.playing) {
|
||||
// Sync with the controls playing
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
// Sync with the controls pause
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackReady() {
|
||||
try {
|
||||
log.info('onPlaybackReady: Playing video');
|
||||
controller.value?.play();
|
||||
controller.value?.setVolume(0.9);
|
||||
} catch (_) {
|
||||
log.info('onPlaybackReady: Played video');
|
||||
} catch (error) {
|
||||
log.severe('Error playing video: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackPositionChanged() {
|
||||
updateVideoPlayback();
|
||||
if (controller.value == null || !context.mounted) {
|
||||
log.info('No controller to update');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Updating video playback');
|
||||
final videoPlayback =
|
||||
VideoPlaybackValue.fromNativeController(controller.value!);
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
log.info('Updated video playback');
|
||||
|
||||
// Check if the video is buffering
|
||||
if (videoPlayback.state == VideoPlaybackState.playing) {
|
||||
log.info('Updating video: checking if playing video is buffering');
|
||||
isBuffering.value =
|
||||
lastVideoPosition.value == videoPlayback.position.inSeconds;
|
||||
lastVideoPosition.value = videoPlayback.position.inSeconds;
|
||||
log.info('Updating playing video position');
|
||||
} else {
|
||||
log.info('Updating video: video is not playing');
|
||||
isBuffering.value = false;
|
||||
lastVideoPosition.value = -1;
|
||||
log.info('Updated non-playing video position');
|
||||
}
|
||||
final state = videoPlayback.state;
|
||||
|
||||
// Enable the WakeLock while the video is playing
|
||||
if (state == VideoPlaybackState.playing) {
|
||||
log.info('Syncing with the controls playing');
|
||||
// Sync with the controls playing
|
||||
WakelockPlus.enable();
|
||||
log.info('Synced with the controls playing');
|
||||
} else {
|
||||
log.info('Syncing with the controls pause');
|
||||
// Sync with the controls pause
|
||||
WakelockPlus.disable();
|
||||
log.info('Synced with the controls pause');
|
||||
}
|
||||
}
|
||||
|
||||
void onPlaybackEnded() {
|
||||
try {
|
||||
log.info('onPlaybackEnded: Video ended');
|
||||
if (loopVideo) {
|
||||
log.info('onPlaybackEnded: Looping video');
|
||||
controller.value?.play();
|
||||
log.info('onPlaybackEnded: Looped video');
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
log.severe('Error looping video: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initController(NativeVideoPlayerController nc) async {
|
||||
if (controller.value != null) {
|
||||
log.info('initController: Controller already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('initController: adding onPlaybackPositionChanged listener');
|
||||
nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged);
|
||||
log.info('initController: added onPlaybackPositionChanged listener');
|
||||
|
||||
log.info('initController: adding onPlaybackStatusChanged listener');
|
||||
nc.onPlaybackStatusChanged.addListener(onPlaybackPositionChanged);
|
||||
log.info('initController: added onPlaybackStatusChanged listener');
|
||||
|
||||
log.info('initController: adding onPlaybackReady listener');
|
||||
nc.onPlaybackReady.addListener(onPlaybackReady);
|
||||
log.info('initController: added onPlaybackReady listener');
|
||||
|
||||
log.info('initController: adding onPlaybackEnded listener');
|
||||
nc.onPlaybackEnded.addListener(onPlaybackEnded);
|
||||
log.info('initController: added onPlaybackEnded listener');
|
||||
|
||||
final videoSource = await createSource(asset);
|
||||
nc.loadVideoSource(videoSource);
|
||||
log.info('initController: loading video source');
|
||||
nc.loadVideoSource(await videoSource);
|
||||
log.info('initController: loaded video source');
|
||||
|
||||
log.info('initController: setting controller');
|
||||
controller.value = nc;
|
||||
log.info('initController: set controller');
|
||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
Future.microtask(
|
||||
() => ref.read(videoPlayerControlsProvider.notifier).reset(),
|
||||
);
|
||||
log.info('useEffect: resetting video player controls');
|
||||
ref.read(videoPlayerControlsProvider.notifier).reset();
|
||||
log.info('useEffect: resetting video player controls');
|
||||
|
||||
if (isMotionVideo) {
|
||||
// ignore: prefer-extracting-callbacks
|
||||
Future.microtask(() {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
});
|
||||
log.info('useEffect: disabled showing video player controls');
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
log.info('useEffect: disabled showing video player controls');
|
||||
}
|
||||
|
||||
return () {
|
||||
bufferingTimer.value.cancel();
|
||||
try {
|
||||
controller.value?.onPlaybackPositionChanged
|
||||
final playerController = controller.value;
|
||||
if (playerController == null) {
|
||||
log.info('No controller to dispose');
|
||||
return;
|
||||
}
|
||||
log.info('Removing onPlaybackPositionChanged listener');
|
||||
playerController.onPlaybackPositionChanged
|
||||
.removeListener(onPlaybackPositionChanged);
|
||||
controller.value?.onPlaybackStatusChanged
|
||||
log.info('Removed onPlaybackPositionChanged listener');
|
||||
|
||||
log.info('Removing onPlaybackStatusChanged listener');
|
||||
playerController.onPlaybackStatusChanged
|
||||
.removeListener(onPlaybackPositionChanged);
|
||||
controller.value?.onPlaybackReady.removeListener(onPlaybackReady);
|
||||
controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded);
|
||||
controller.value?.stop();
|
||||
} catch (_) {
|
||||
log.info('Removed onPlaybackStatusChanged listener');
|
||||
|
||||
log.info('Removing onPlaybackReady listener');
|
||||
playerController.onPlaybackReady.removeListener(onPlaybackReady);
|
||||
log.info('Removed onPlaybackReady listener');
|
||||
|
||||
log.info('Removing onPlaybackEnded listener');
|
||||
playerController.onPlaybackEnded.removeListener(onPlaybackEnded);
|
||||
log.info('Removed onPlaybackEnded listener');
|
||||
|
||||
Future.microtask(() async {
|
||||
log.info('Stopping video');
|
||||
await playerController.stop();
|
||||
log.info('Stopped video');
|
||||
|
||||
log.info('Disabling wakelock');
|
||||
await WakelockPlus.disable();
|
||||
log.info('Disabled wakelock');
|
||||
});
|
||||
|
||||
log.info('Disposing controller');
|
||||
controller.value = null;
|
||||
log.info('Disposed controller');
|
||||
} catch (error) {
|
||||
log.severe('Error during useEffect cleanup: $error');
|
||||
// Consume error from the controller
|
||||
}
|
||||
};
|
||||
@ -244,63 +302,33 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
double calculateAspectRatio() {
|
||||
if (width.value == 0 || height.value == 0) {
|
||||
return 1;
|
||||
}
|
||||
return width.value / height.value;
|
||||
}
|
||||
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
||||
return SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) => ref
|
||||
.read(videoPlaybackValueProvider.notifier)
|
||||
.value = VideoPlaybackValue.uninitialized(),
|
||||
child: SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: calculateAspectRatio(),
|
||||
child: NativeVideoPlayerView(
|
||||
onViewReady: initController,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showControls)
|
||||
Center(
|
||||
child: CustomVideoPlayerControls(
|
||||
hideTimerDuration: hideControlsTimer,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: controller.value == null,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (placeholder != null) placeholder!,
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: NativeVideoPlayerView(
|
||||
onViewReady: initController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showControls)
|
||||
Center(
|
||||
child: CustomVideoPlayerControls(
|
||||
hideTimerDuration: hideControlsTimer,
|
||||
),
|
||||
),
|
||||
// Visibility(
|
||||
// visible: controller.value == null,
|
||||
// child: const Positioned.fill(
|
||||
// child: Center(
|
||||
// child: DelayedLoadingIndicator(
|
||||
// fadeInDuration: Duration(milliseconds: 500),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
18
mobile/lib/utils/hooks/interval_hook.dart
Normal file
18
mobile/lib/utils/hooks/interval_hook.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
// https://github.com/rrousselGit/flutter_hooks/issues/233#issuecomment-840416638
|
||||
void useInterval(Duration delay, VoidCallback callback) {
|
||||
final savedCallback = useRef(callback);
|
||||
savedCallback.value = callback;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
final timer = Timer.periodic(delay, (_) => savedCallback.value());
|
||||
return timer.cancel;
|
||||
},
|
||||
[delay],
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_loader.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
@ -68,8 +69,8 @@ class MemoryCard extends StatelessWidget {
|
||||
} else {
|
||||
return Hero(
|
||||
tag: 'memory-${asset.id}',
|
||||
child: NativeVideoViewerPage(
|
||||
key: ValueKey(asset),
|
||||
child: NativeVideoLoader(
|
||||
key: ValueKey(asset.id),
|
||||
asset: asset,
|
||||
placeholder: SizedBox.expand(
|
||||
child: ImmichImage(
|
||||
|
Loading…
x
Reference in New Issue
Block a user