From be0fe36210d11543afe813009914d5ff31ecfb97 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:55:48 -0400 Subject: [PATCH] preserve subtree not growable minor cleanup --- .../alextran/immich/images/Thumbnails.g.kt | 6 +-- .../alextran/immich/images/ThumbnailsImpl.kt | 6 +-- mobile/ios/Runner/Images/Thumbnails.g.swift | 10 ++--- mobile/ios/Runner/Images/ThumbnailsImpl.swift | 2 +- .../repositories/asset_media.repository.dart | 2 +- mobile/lib/platform/thumbnail_api.g.dart | 28 +++++--------- .../widgets/images/image_provider.dart | 2 +- .../widgets/images/local_image_provider.dart | 8 ++-- .../widgets/timeline/fixed/segment.model.dart | 38 +++++++------------ .../widgets/timeline/timeline.widget.dart | 1 - mobile/pigeon/thumbnail_api.dart | 2 +- 11 files changed, 40 insertions(+), 65 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt index 3222fc6f2c..a70de4300b 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt @@ -59,7 +59,7 @@ private open class ThumbnailsPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface ThumbnailApi { - fun setThumbnailToBuffer(assetId: String, width: Long, height: Long, callback: (Result>) -> Unit) + fun getThumbnailBuffer(assetId: String, width: Long, height: Long, callback: (Result>) -> Unit) companion object { /** The codec used by ThumbnailApi. */ @@ -71,14 +71,14 @@ interface ThumbnailApi { fun setUp(binaryMessenger: BinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.setThumbnailToBuffer$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnailBuffer$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val assetIdArg = args[0] as String val widthArg = args[1] as Long val heightArg = args[2] as Long - api.setThumbnailToBuffer(assetIdArg, widthArg, heightArg) { result: Result> -> + api.getThumbnailBuffer(assetIdArg, widthArg, heightArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { reply.reply(ThumbnailsPigeonUtils.wrapError(error)) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt index ccf0995150..804cdcb3c1 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt @@ -49,19 +49,19 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { external fun wrapAsBuffer(address: Long, capacity: Int): ByteBuffer } - override fun setThumbnailToBuffer( + override fun getThumbnailBuffer( assetId: String, width: Long, height: Long, callback: (Result>) -> Unit ) { threadPool.execute { try { - setThumbnailToBufferInternal(assetId, width, height, callback) + getThumbnailBufferInternal(assetId, width, height, callback) } catch (e: Exception) { callback(Result.failure(e)) } } } - private fun setThumbnailToBufferInternal( + private fun getThumbnailBufferInternal( assetId: String, width: Long, height: Long, callback: (Result>) -> Unit ) { val targetWidth = width.toInt() diff --git a/mobile/ios/Runner/Images/Thumbnails.g.swift b/mobile/ios/Runner/Images/Thumbnails.g.swift index 3f8b1e693b..6fbf28a474 100644 --- a/mobile/ios/Runner/Images/Thumbnails.g.swift +++ b/mobile/ios/Runner/Images/Thumbnails.g.swift @@ -70,7 +70,7 @@ class ThumbnailsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol ThumbnailApi { - func setThumbnailToBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void) + func getThumbnailBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -79,14 +79,14 @@ class ThumbnailApiSetup { /// Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let setThumbnailToBufferChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.setThumbnailToBuffer\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getThumbnailBufferChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnailBuffer\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - setThumbnailToBufferChannel.setMessageHandler { message, reply in + getThumbnailBufferChannel.setMessageHandler { message, reply in let args = message as! [Any?] let assetIdArg = args[0] as! String let widthArg = args[1] as! Int64 let heightArg = args[2] as! Int64 - api.setThumbnailToBuffer(assetId: assetIdArg, width: widthArg, height: heightArg) { result in + api.getThumbnailBuffer(assetId: assetIdArg, width: widthArg, height: heightArg) { result in switch result { case .success(let res): reply(wrapResult(res)) @@ -96,7 +96,7 @@ class ThumbnailApiSetup { } } } else { - setThumbnailToBufferChannel.setMessageHandler(nil) + getThumbnailBufferChannel.setMessageHandler(nil) } } } diff --git a/mobile/ios/Runner/Images/ThumbnailsImpl.swift b/mobile/ios/Runner/Images/ThumbnailsImpl.swift index af7d508046..b774572268 100644 --- a/mobile/ios/Runner/Images/ThumbnailsImpl.swift +++ b/mobile/ios/Runner/Images/ThumbnailsImpl.swift @@ -23,7 +23,7 @@ class ThumbnailApiImpl: ThumbnailApi { private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB() private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue - func setThumbnailToBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { + func getThumbnailBuffer(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { Self.processingQueue.async { guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject else { completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))); return } diff --git a/mobile/lib/infrastructure/repositories/asset_media.repository.dart b/mobile/lib/infrastructure/repositories/asset_media.repository.dart index 13b2b818c6..11a0d5e0cb 100644 --- a/mobile/lib/infrastructure/repositories/asset_media.repository.dart +++ b/mobile/lib/infrastructure/repositories/asset_media.repository.dart @@ -11,7 +11,7 @@ class AssetMediaRepository { const AssetMediaRepository(); Future getLocalThumbnail(String localId, ui.Size size) async { - final info = await thumbnailApi.setThumbnailToBuffer( + final info = await thumbnailApi.getThumbnailBuffer( localId, width: size.width.toInt(), height: size.height.toInt(), diff --git a/mobile/lib/platform/thumbnail_api.g.dart b/mobile/lib/platform/thumbnail_api.g.dart index ac5249d6c7..de598d3411 100644 --- a/mobile/lib/platform/thumbnail_api.g.dart +++ b/mobile/lib/platform/thumbnail_api.g.dart @@ -40,34 +40,25 @@ class ThumbnailApi { /// Constructor for [ThumbnailApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ThumbnailApi( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + ThumbnailApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future> setThumbnailToBuffer( - String assetId, { - required int width, - required int height, - }) async { + Future> getThumbnailBuffer(String assetId, {required int width, required int height}) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.setThumbnailToBuffer$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnailBuffer$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([assetId, width, height]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetId, width, height]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -82,8 +73,7 @@ class ThumbnailApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as Map?)! - .cast(); + return (pigeonVar_replyList[0] as Map?)!.cast(); } } } diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index d87920fdbe..0b45e53405 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -37,7 +37,7 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz if (_shouldUseLocalAsset(asset!)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; - return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, size: size); + return LocalThumbProvider(id: id, size: size); } final String assetId; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index c808682b56..839bd070f5 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -10,9 +10,8 @@ class LocalThumbProvider extends ImageProvider { final String id; final Size size; - final DateTime? updatedAt; - const LocalThumbProvider({required this.id, required this.size, this.updatedAt}); + const LocalThumbProvider({required this.id, required this.size}); @override Future obtainKey(ImageConfiguration configuration) { @@ -25,7 +24,6 @@ class LocalThumbProvider extends ImageProvider { _codec(key), informationCollector: () => [ DiagnosticsProperty('Id', key.id), - DiagnosticsProperty('Updated at', key.updatedAt), DiagnosticsProperty('Size', key.size), ], ); @@ -40,13 +38,13 @@ class LocalThumbProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalThumbProvider) { - return id == other.id && size == other.size && updatedAt == other.updatedAt; + return id == other.id && size == other.size; } return false; } @override - int get hashCode => id.hashCode ^ updatedAt.hashCode; + int get hashCode => id.hashCode ^ size.hashCode; } class LocalFullImageProvider extends ImageProvider { diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index fdb5ddf692..295df60f25 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -112,20 +112,9 @@ class _FixedSegmentRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final timelineService = ref.read(timelineServiceProvider); - - if (timelineService.hasRange(assetIndex, assetCount)) { - return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService); - } - try { final assets = timelineService.getAssets(assetIndex, assetCount); - return FutureBuilder>( - future: null, - initialData: assets, - builder: (context, snapshot) { - return _buildAssetRow(context, snapshot.data, timelineService); - }, - ); + return _buildAssetRow(context, assets, timelineService); } catch (e) { return FutureBuilder>( future: timelineService.loadAssets(assetIndex, assetCount), @@ -137,22 +126,21 @@ class _FixedSegmentRow extends ConsumerWidget { } Widget _buildAssetRow(BuildContext context, List? assets, TimelineService timelineService) { + final assetIndex = this.assetIndex; return FixedTimelineRow( dimension: tileHeight, spacing: spacing, textDirection: Directionality.of(context), - children: [ - for (int i = 0; i < assetCount; i++) - TimelineAssetIndexWrapper( - assetIndex: assetIndex + i, - segmentIndex: 0, // For simplicity, using 0 for now - child: _AssetTileWidget( - key: ValueKey(i.hashCode ^ (assetIndex + i).hashCode ^ timelineService.hashCode), - asset: assets == null ? null : assets[i], - assetIndex: assetIndex + i, - ), - ), - ], + children: List.generate(assetCount, (i) { + final curAssetIndex = assetIndex + i; + return TimelineAssetIndexWrapper( + // this key is intentionally generic to preserve the state of the widget and its subtree + key: ValueKey(i.hashCode ^ timelineService.hashCode), + assetIndex: curAssetIndex, + segmentIndex: 0, // For simplicity, using 0 for now + child: _AssetTileWidget(asset: assets?[i], assetIndex: curAssetIndex), + ); + }, growable: false), ); } } @@ -161,7 +149,7 @@ class _AssetTileWidget extends ConsumerWidget { final BaseAsset? asset; final int assetIndex; - const _AssetTileWidget({super.key, required this.asset, required this.assetIndex}); + const _AssetTileWidget({required this.asset, required this.assetIndex}); Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async { final multiSelectState = ref.read(multiSelectProvider); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 4af44adb42..c859ae0e80 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -101,7 +101,6 @@ class _SliverTimeline extends ConsumerStatefulWidget { class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final _scrollController = ScrollController(); StreamSubscription? _eventSubscription; - // late final KeepAliveLink asyncSegmentsLink; // Drag selection state bool _dragging = false; diff --git a/mobile/pigeon/thumbnail_api.dart b/mobile/pigeon/thumbnail_api.dart index 04a56b3a14..2072c14b11 100644 --- a/mobile/pigeon/thumbnail_api.dart +++ b/mobile/pigeon/thumbnail_api.dart @@ -15,7 +15,7 @@ import 'package:pigeon/pigeon.dart'; @HostApi() abstract class ThumbnailApi { @async - Map setThumbnailToBuffer( + Map getThumbnailBuffer( String assetId, { required int width, required int height,