mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
fixed memory leak
This commit is contained in:
parent
7ed2c68c46
commit
b461318641
@ -47,7 +47,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
this.initialIndex = 0,
|
this.initialIndex = 0,
|
||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
}) : controller = PageController(initialPage: initialIndex);
|
}) : controller = PageController(initialPage: initialIndex, keepPage: false);
|
||||||
|
|
||||||
final PageController controller;
|
final PageController controller;
|
||||||
|
|
||||||
@ -56,16 +56,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final settings = ref.watch(appSettingsServiceProvider);
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
final loadAsset = renderList.loadAsset;
|
final loadAsset = renderList.loadAsset;
|
||||||
final totalAssets = useState(renderList.totalAssets);
|
final totalAssets = useState(renderList.totalAssets);
|
||||||
final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
|
final shouldLoopVideo =
|
||||||
|
useState(settings.getSetting<bool>(AppSettingsEnum.loopVideo));
|
||||||
final isZoomed = useState(false);
|
final isZoomed = useState(false);
|
||||||
final isPlayingVideo = useState(false);
|
final isPlayingVideo = useState(false);
|
||||||
final localPosition = useState<Offset?>(null);
|
final localPosition = useRef<Offset?>(null);
|
||||||
final currentIndex = useState(initialIndex);
|
final currentIndex = useValueNotifier(initialIndex);
|
||||||
final currentAsset = loadAsset(currentIndex.value);
|
final currentAsset = loadAsset(currentIndex.value);
|
||||||
// Update is playing motion video
|
|
||||||
ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) {
|
|
||||||
isPlayingVideo.value = state == VideoPlaybackState.playing;
|
|
||||||
});
|
|
||||||
|
|
||||||
final stackIndex = useState(-1);
|
final stackIndex = useState(-1);
|
||||||
final stack = showStack && currentAsset.stackCount > 0
|
final stack = showStack && currentAsset.stackCount > 0
|
||||||
@ -80,28 +77,26 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
: stackElements.elementAt(stackIndex.value);
|
: stackElements.elementAt(stackIndex.value);
|
||||||
|
|
||||||
final isMotionPhoto = asset.livePhotoVideoId != null;
|
final isMotionPhoto = asset.livePhotoVideoId != null;
|
||||||
|
// Update is playing motion video
|
||||||
|
if (isMotionPhoto) {
|
||||||
|
ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) {
|
||||||
|
isPlayingVideo.value = state == VideoPlaybackState.playing;
|
||||||
|
});
|
||||||
|
}
|
||||||
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
|
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
|
||||||
ref.listen(currentAssetProvider, (_, __) {});
|
ref.listen(currentAssetProvider, (_, __) {});
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
// Delay state update to after the execution of build method
|
// Delay state update to after the execution of build method
|
||||||
Future.microtask(
|
ref.read(currentAssetProvider.notifier).set(asset);
|
||||||
() => ref.read(currentAssetProvider.notifier).set(asset),
|
// Future.microtask(
|
||||||
);
|
// () => ref.read(currentAssetProvider.notifier).set(asset),
|
||||||
|
// );
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[asset],
|
[asset],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
shouldLoopVideo.value =
|
|
||||||
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> precacheNextImage(int index) async {
|
Future<void> precacheNextImage(int index) async {
|
||||||
void onError(Object exception, StackTrace? stackTrace) {
|
void onError(Object exception, StackTrace? stackTrace) {
|
||||||
// swallow error silently
|
// swallow error silently
|
||||||
@ -110,6 +105,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (index < totalAssets.value && index >= 0) {
|
if (index < totalAssets.value && index >= 0) {
|
||||||
|
log.info('Precaching next image at index $index');
|
||||||
final asset = loadAsset(index);
|
final asset = loadAsset(index);
|
||||||
await precacheImage(
|
await precacheImage(
|
||||||
ImmichImage.imageProvider(asset: asset),
|
ImmichImage.imageProvider(asset: asset),
|
||||||
@ -189,7 +185,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
}
|
}
|
||||||
isPlayingVideo.value = false;
|
if (isMotionPhoto) {
|
||||||
|
isPlayingVideo.value = false;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@ -275,6 +273,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||||
},
|
},
|
||||||
|
// wantKeepAlive: true,
|
||||||
|
// gaplessPlayback: true,
|
||||||
loadingBuilder: (context, event, index) => ClipRect(
|
loadingBuilder: (context, event, index) => ClipRect(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
@ -302,13 +302,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
itemCount: totalAssets.value,
|
itemCount: totalAssets.value,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) async {
|
onPageChanged: (value) async {
|
||||||
|
log.info('Page changed to $value');
|
||||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
|
||||||
|
log.info('Setting current index to $value');
|
||||||
currentIndex.value = value;
|
currentIndex.value = value;
|
||||||
stackIndex.value = -1;
|
if (stackIndex.value != -1) {
|
||||||
isPlayingVideo.value = false;
|
stackIndex.value = -1;
|
||||||
|
}
|
||||||
|
if (isMotionPhoto) {
|
||||||
|
isPlayingVideo.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for page change animation to finish
|
// Wait for page change animation to finish
|
||||||
await Future.delayed(const Duration(milliseconds: 400));
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
@ -324,15 +330,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (a.isImage && !isPlayingVideo.value) {
|
if (a.isImage && !isPlayingVideo.value) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
onDragStart: (_, details, __) =>
|
onDragStart: (_, details, __) {
|
||||||
localPosition.value = details.localPosition,
|
log.info('Drag start');
|
||||||
onDragUpdate: (_, details, __) =>
|
localPosition.value = details.localPosition;
|
||||||
handleSwipeUpDown(details),
|
},
|
||||||
|
onDragUpdate: (_, details, __) {
|
||||||
|
log.info('Drag update');
|
||||||
|
handleSwipeUpDown(details);
|
||||||
|
},
|
||||||
onTapDown: (_, __, ___) {
|
onTapDown: (_, __, ___) {
|
||||||
ref.read(showControlsProvider.notifier).toggle();
|
ref.read(showControlsProvider.notifier).toggle();
|
||||||
},
|
},
|
||||||
onLongPressStart: (_, __, ___) {
|
onLongPressStart: (_, __, ___) {
|
||||||
if (asset.livePhotoVideoId != null) {
|
if (isMotionPhoto) {
|
||||||
isPlayingVideo.value = true;
|
isPlayingVideo.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -352,24 +362,28 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
log.info('Loading asset ${a.id} (index $index) as video');
|
||||||
|
ref.read(videoPlaybackValueProvider.notifier).value =
|
||||||
|
VideoPlaybackValue.uninitialized();
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
onDragStart: (_, details, __) =>
|
// onDragStart: (_, details, __) =>
|
||||||
localPosition.value = details.localPosition,
|
// localPosition.value = details.localPosition,
|
||||||
onDragUpdate: (_, details, __) =>
|
// onDragUpdate: (_, details, __) =>
|
||||||
handleSwipeUpDown(details),
|
// handleSwipeUpDown(details),
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
// heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: isFromDto
|
// tag: isFromDto
|
||||||
? '${currentAsset.remoteId}-$heroOffset'
|
// ? '${currentAsset.remoteId}-$heroOffset'
|
||||||
: currentAsset.id + heroOffset,
|
// : currentAsset.id + heroOffset,
|
||||||
),
|
// ),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: 1.0,
|
||||||
maxScale: 1.0,
|
maxScale: 1.0,
|
||||||
minScale: 1.0,
|
minScale: 1.0,
|
||||||
basePosition: Alignment.center,
|
basePosition: Alignment.center,
|
||||||
child: NativeVideoLoader(
|
child: NativeVideoLoader(
|
||||||
key: ValueKey(a.id),
|
key: ValueKey(a.id),
|
||||||
asset: a,
|
asset: a,
|
||||||
isMotionVideo: a.livePhotoVideoId != null,
|
isMotionVideo: isMotionPhoto,
|
||||||
loopVideo: shouldLoopVideo.value,
|
loopVideo: shouldLoopVideo.value,
|
||||||
placeholder: Image(
|
placeholder: Image(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
@ -50,10 +50,10 @@ class NativeVideoLoader extends HookConsumerWidget {
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
final localEntity = useMemoized(
|
// final localEntity = useMemoized(
|
||||||
() => asset.isLocal ? AssetEntity.fromId(asset.localId!) : null,
|
// () => asset.isLocal ? AssetEntity.fromId(asset.localId!) : null,
|
||||||
);
|
// );
|
||||||
Future<double> calculateAspectRatio() async {
|
Future<double> calculateAspectRatio(AssetEntity? localEntity) async {
|
||||||
log.info('Calculating aspect ratio');
|
log.info('Calculating aspect ratio');
|
||||||
late final double? orientatedWidth;
|
late final double? orientatedWidth;
|
||||||
late final double? orientatedHeight;
|
late final double? orientatedHeight;
|
||||||
@ -62,9 +62,8 @@ class NativeVideoLoader extends HookConsumerWidget {
|
|||||||
orientatedWidth = asset.orientatedWidth?.toDouble();
|
orientatedWidth = asset.orientatedWidth?.toDouble();
|
||||||
orientatedHeight = asset.orientatedHeight?.toDouble();
|
orientatedHeight = asset.orientatedHeight?.toDouble();
|
||||||
} else if (localEntity != null) {
|
} else if (localEntity != null) {
|
||||||
final entity = await localEntity;
|
orientatedWidth = localEntity.orientatedWidth.toDouble();
|
||||||
orientatedWidth = entity?.orientatedWidth.toDouble();
|
orientatedHeight = localEntity.orientatedHeight.toDouble();
|
||||||
orientatedHeight = entity?.orientatedHeight.toDouble();
|
|
||||||
} else {
|
} else {
|
||||||
final entity = await ref.read(assetServiceProvider).loadExif(asset);
|
final entity = await ref.read(assetServiceProvider).loadExif(asset);
|
||||||
orientatedWidth = entity.orientatedWidth?.toDouble();
|
orientatedWidth = entity.orientatedWidth?.toDouble();
|
||||||
@ -82,16 +81,15 @@ class NativeVideoLoader extends HookConsumerWidget {
|
|||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final aspectRatioFuture = useMemoized(() => calculateAspectRatio());
|
// final aspectRatioFuture = useMemoized(() => calculateAspectRatio());
|
||||||
|
|
||||||
Future<VideoSource> createLocalSource() async {
|
Future<VideoSource> createLocalSource(AssetEntity? localEntity) async {
|
||||||
log.info('Loading video from local storage');
|
log.info('Loading video from local storage');
|
||||||
final entity = await localEntity;
|
if (localEntity == null) {
|
||||||
if (entity == null) {
|
|
||||||
throw Exception('No entity found for the video');
|
throw Exception('No entity found for the video');
|
||||||
}
|
}
|
||||||
|
|
||||||
final file = await entity.file;
|
final file = await localEntity.file;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw Exception('No file found for the video');
|
throw Exception('No file found for the video');
|
||||||
}
|
}
|
||||||
@ -122,24 +120,51 @@ class NativeVideoLoader extends HookConsumerWidget {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<VideoSource> createSource() {
|
Future<VideoSource> createSource(AssetEntity? localEntity) {
|
||||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
if (localEntity != null && asset.livePhotoVideoId == null) {
|
||||||
return createLocalSource();
|
return createLocalSource(localEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createRemoteSource();
|
return createRemoteSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
final createSourceFuture = useMemoized(() => createSource());
|
// final createSourceFuture = useMemoized(() => createSource());
|
||||||
|
|
||||||
final combinedFuture = useMemoized(
|
final combinedFuture = useMemoized(
|
||||||
() async {
|
() => Future.delayed(Duration(milliseconds: 1), () async {
|
||||||
final aspectRatio = await aspectRatioFuture;
|
if (!context.mounted) {
|
||||||
final source = await createSourceFuture;
|
return null;
|
||||||
return (source, aspectRatio);
|
}
|
||||||
},
|
|
||||||
|
final entity =
|
||||||
|
asset.isLocal ? await AssetEntity.fromId(asset.localId!) : null;
|
||||||
|
return (createSource(entity), calculateAspectRatio(entity)).wait;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final doCleanup = useState(false);
|
||||||
|
ref.listen(videoPlaybackValueProvider.select((value) => value.state),
|
||||||
|
(_, value) {
|
||||||
|
if (value == VideoPlaybackState.initializing) {
|
||||||
|
log.info('Cleaning up video');
|
||||||
|
doCleanup.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// useEffect(() {
|
||||||
|
// Future.microtask(() {
|
||||||
|
// if (!context.mounted) {
|
||||||
|
// return Future.value(null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (createSourceFuture, aspectRatioFuture).wait;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return () {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }, [asset.id]);
|
||||||
|
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@ -154,25 +179,27 @@ class NativeVideoLoader extends HookConsumerWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: size.height,
|
height: size.height,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
child: FutureBuilder(
|
child: doCleanup.value
|
||||||
key: ValueKey(asset.id),
|
? placeholder
|
||||||
future: combinedFuture,
|
: FutureBuilder(
|
||||||
// initialData: initAspectRatio,
|
key: ValueKey(asset.id),
|
||||||
builder: (context, snapshot) {
|
future: combinedFuture,
|
||||||
if (!snapshot.hasData) {
|
// initialData: initAspectRatio,
|
||||||
return placeholder;
|
builder: (context, snapshot) {
|
||||||
}
|
if (!snapshot.hasData) {
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
return NativeVideoViewerPage(
|
return NativeVideoViewerPage(
|
||||||
videoSource: snapshot.data!.$1,
|
videoSource: snapshot.data!.$1,
|
||||||
duration: asset.duration,
|
aspectRatio: snapshot.data!.$2,
|
||||||
aspectRatio: snapshot.data!.$2,
|
duration: asset.duration,
|
||||||
isMotionVideo: isMotionVideo,
|
isMotionVideo: isMotionVideo,
|
||||||
hideControlsTimer: hideControlsTimer,
|
hideControlsTimer: hideControlsTimer,
|
||||||
loopVideo: loopVideo,
|
loopVideo: loopVideo,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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_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/utils/hooks/blurhash_hook.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user