diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 218c0b2832..81af41ab08 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -61,7 +61,6 @@ import UIKit BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl()) ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl()) NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller)) - ViewIntentHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ViewIntentApiImpl()) } public static func cancelPlugins(with engine: FlutterEngine) { diff --git a/mobile/ios/Runner/ViewIntent/ViewIntent.g.swift b/mobile/ios/Runner/ViewIntent/ViewIntent.g.swift deleted file mode 100644 index b28332976c..0000000000 --- a/mobile/ios/Runner/ViewIntent/ViewIntent.g.swift +++ /dev/null @@ -1,228 +0,0 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -func deepEqualsViewIntent(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case is (Void, Void): - return true - - case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): - return cleanLhsHashable == cleanRhsHashable - - case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): - guard cleanLhsArray.count == cleanRhsArray.count else { return false } - for (index, element) in cleanLhsArray.enumerated() { - if !deepEqualsViewIntent(element, cleanRhsArray[index]) { - return false - } - } - return true - - case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } - for (key, cleanLhsValue) in cleanLhsDictionary { - guard cleanRhsDictionary.index(forKey: key) != nil else { return false } - if !deepEqualsViewIntent(cleanLhsValue, cleanRhsDictionary[key]!) { - return false - } - } - return true - - default: - // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. - return false - } -} - -func deepHashViewIntent(value: Any?, hasher: inout Hasher) { - if let valueList = value as? [AnyHashable] { - for item in valueList { deepHashViewIntent(value: item, hasher: &hasher) } - return - } - - if let valueDict = value as? [AnyHashable: AnyHashable] { - for key in valueDict.keys { - hasher.combine(key) - deepHashViewIntent(value: valueDict[key]!, hasher: &hasher) - } - return - } - - if let hashableValue = value as? AnyHashable { - hasher.combine(hashableValue.hashValue) - } - - return hasher.combine(String(describing: value)) -} - - - -enum ViewIntentType: Int { - case image = 0 - case video = 1 -} - -/// Generated class from Pigeon that represents data sent in messages. -struct ViewIntentPayload: Hashable { - var path: String - var type: ViewIntentType - var mimeType: String - var localAssetId: String? = nil - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> ViewIntentPayload? { - let path = pigeonVar_list[0] as! String - let type = pigeonVar_list[1] as! ViewIntentType - let mimeType = pigeonVar_list[2] as! String - let localAssetId: String? = nilOrValue(pigeonVar_list[3]) - - return ViewIntentPayload( - path: path, - type: type, - mimeType: mimeType, - localAssetId: localAssetId - ) - } - func toList() -> [Any?] { - return [ - path, - type, - mimeType, - localAssetId, - ] - } - static func == (lhs: ViewIntentPayload, rhs: ViewIntentPayload) -> Bool { - return deepEqualsViewIntent(lhs.toList(), rhs.toList()) } - func hash(into hasher: inout Hasher) { - deepHashViewIntent(value: toList(), hasher: &hasher) - } -} - -private class ViewIntentPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return ViewIntentType(rawValue: enumResultAsInt) - } - return nil - case 130: - return ViewIntentPayload.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class ViewIntentPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? ViewIntentType { - super.writeByte(129) - super.writeValue(value.rawValue) - } else if let value = value as? ViewIntentPayload { - super.writeByte(130) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class ViewIntentPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return ViewIntentPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return ViewIntentPigeonCodecWriter(data: data) - } -} - -class ViewIntentPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = ViewIntentPigeonCodec(readerWriter: ViewIntentPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol ViewIntentHostApi { - func consumeViewIntent(completion: @escaping (Result) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class ViewIntentHostApiSetup { - static var codec: FlutterStandardMessageCodec { ViewIntentPigeonCodec.shared } - /// Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let consumeViewIntentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - consumeViewIntentChannel.setMessageHandler { _, reply in - api.consumeViewIntent { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - consumeViewIntentChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/ViewIntent/ViewIntentApiImpl.swift b/mobile/ios/Runner/ViewIntent/ViewIntentApiImpl.swift deleted file mode 100644 index 07ff78a582..0000000000 --- a/mobile/ios/Runner/ViewIntent/ViewIntentApiImpl.swift +++ /dev/null @@ -1,5 +0,0 @@ -class ViewIntentApiImpl: ViewIntentHostApi { - func consumeViewIntent(completion: @escaping (Result) -> Void) { - completion(.success(nil)) - } -} diff --git a/mobile/lib/models/view_intent/view_intent_attachment.model.dart b/mobile/lib/models/view_intent/view_intent_attachment.model.dart index 92f2b2f88c..c4bb4c259f 100644 --- a/mobile/lib/models/view_intent/view_intent_attachment.model.dart +++ b/mobile/lib/models/view_intent/view_intent_attachment.model.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:path/path.dart'; enum ViewIntentAttachmentType { image, video } @@ -7,9 +8,15 @@ enum ViewIntentAttachmentType { image, video } class ViewIntentAttachment { final String path; final ViewIntentAttachmentType type; + final String mimeType; final String? localAssetId; - const ViewIntentAttachment({required this.path, required this.type, this.localAssetId}); + const ViewIntentAttachment({ + required this.path, + required this.type, + required this.mimeType, + this.localAssetId, + }); File get file => File(path); @@ -18,4 +25,22 @@ class ViewIntentAttachment { bool get isImage => type == ViewIntentAttachmentType.image; bool get isVideo => type == ViewIntentAttachmentType.video; + + AssetPlaybackStyle get playbackStyle { + if (isVideo) { + return AssetPlaybackStyle.video; + } + + final normalizedMimeType = mimeType.toLowerCase(); + if (normalizedMimeType == 'image/gif' || normalizedMimeType == 'image/webp') { + return AssetPlaybackStyle.imageAnimated; + } + + final normalizedPath = path.toLowerCase(); + if (normalizedPath.endsWith('.gif') || normalizedPath.endsWith('.webp')) { + return AssetPlaybackStyle.imageAnimated; + } + + return AssetPlaybackStyle.image; + } } diff --git a/mobile/lib/platform/view_intent_api.g.dart b/mobile/lib/platform/view_intent_api.g.dart index bcc1f3a8ee..4190d2cc1c 100644 --- a/mobile/lib/platform/view_intent_api.g.dart +++ b/mobile/lib/platform/view_intent_api.g.dart @@ -14,25 +14,33 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } -enum ViewIntentType { image, video } + +enum ViewIntentType { + image, + video, +} class ViewIntentPayload { - ViewIntentPayload({required this.path, required this.type, required this.mimeType, this.localAssetId}); + ViewIntentPayload({ + required this.path, + required this.type, + required this.mimeType, + this.localAssetId, + }); String path; @@ -43,12 +51,16 @@ class ViewIntentPayload { String? localAssetId; List _toList() { - return [path, type, mimeType, localAssetId]; + return [ + path, + type, + mimeType, + localAssetId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static ViewIntentPayload decode(Object result) { result as List; @@ -74,9 +86,11 @@ class ViewIntentPayload { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -84,10 +98,10 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is ViewIntentType) { + } else if (value is ViewIntentType) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is ViewIntentPayload) { + } else if (value is ViewIntentPayload) { buffer.putUint8(130); writeValue(buffer, value.encode()); } else { @@ -98,10 +112,10 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final int? value = readValue(buffer) as int?; return value == null ? null : ViewIntentType.values[value]; - case 130: + case 130: return ViewIntentPayload.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -114,8 +128,8 @@ class ViewIntentHostApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. ViewIntentHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -123,15 +137,15 @@ class ViewIntentHostApi { final String pigeonVar_messageChannelSuffix; Future consumeViewIntent() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/mobile/lib/providers/asset_viewer/view_intent_handler.provider.dart b/mobile/lib/providers/asset_viewer/view_intent_handler.provider.dart index e4bace8763..b80658c5ac 100644 --- a/mobile/lib/providers/asset_viewer/view_intent_handler.provider.dart +++ b/mobile/lib/providers/asset_viewer/view_intent_handler.provider.dart @@ -7,12 +7,9 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/models/view_intent/view_intent_attachment.model.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -89,8 +86,8 @@ class ViewIntentHandler { return; } } - - final fallbackAsset = _toViewIntentAsset(attachment); + final checksum = localAssetId != null ? await _computeChecksum(localAssetId) : null; + final fallbackAsset = _toViewIntentAsset(attachment).copyWith(checksum: checksum); _logger.fine('openAssetViewer for fallbackAsset'); _openAssetViewer(fallbackAsset, _timelineFactory.fromAssets([fallbackAsset], TimelineOrigin.deepLink), 0); } @@ -135,13 +132,9 @@ class ViewIntentHandler { void _openAssetViewer(BaseAsset asset, TimelineService timelineService, int initialIndex) { _ref.read(assetViewerProvider.notifier).reset(); _ref.read(assetViewerProvider.notifier).setAsset(asset); - _ref.read(currentAssetNotifier.notifier).setAsset(asset); - if (asset.isVideo || asset.isMotionPhoto) { - _ref.read(videoPlaybackValueProvider.notifier).reset(); - _ref.read(videoPlayerControlsProvider.notifier).pause(); - } + if (asset.isVideo) { - _ref.read(assetViewerProvider.notifier).setControls(false); + _ref.read(assetViewerProvider.notifier).setControls(true); } _router.push(AssetViewerRoute(initialIndex: initialIndex, timelineService: timelineService)); @@ -163,13 +156,14 @@ class ViewIntentHandler { final now = DateTime.now(); return LocalAsset( - id: attachment.path, + id: attachment.localAssetId ?? '', name: attachment.fileName, checksum: null, type: attachment.isVideo ? AssetType.video : AssetType.image, createdAt: now, updatedAt: now, isEdited: false, + playbackStyle: attachment.playbackStyle, ); } } diff --git a/mobile/lib/repositories/view_handler.repository.dart b/mobile/lib/repositories/view_handler.repository.dart index 1feeb40f78..007ca097e9 100644 --- a/mobile/lib/repositories/view_handler.repository.dart +++ b/mobile/lib/repositories/view_handler.repository.dart @@ -18,6 +18,7 @@ class ViewHandlerRepository { return ViewIntentAttachment( path: result.path, type: result.type == ViewIntentType.image ? ViewIntentAttachmentType.image : ViewIntentAttachmentType.video, + mimeType: result.mimeType, localAssetId: result.localAssetId, ); } catch (_) { diff --git a/mobile/pigeon/view_intent_api.dart b/mobile/pigeon/view_intent_api.dart index 1e4f1515ed..2a0ce77d3c 100644 --- a/mobile/pigeon/view_intent_api.dart +++ b/mobile/pigeon/view_intent_api.dart @@ -3,8 +3,6 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/platform/view_intent_api.g.dart', - swiftOut: 'ios/Runner/ViewIntent/ViewIntent.g.swift', - swiftOptions: SwiftOptions(includeErrorClass: false), kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt', kotlinOptions: KotlinOptions(package: 'app.alextran.immich.viewintent'), dartOptions: DartOptions(), diff --git a/mobile/test/services/view_intent_service_test.dart b/mobile/test/services/view_intent_service_test.dart index fa4a940695..9bf6bed0e0 100644 --- a/mobile/test/services/view_intent_service_test.dart +++ b/mobile/test/services/view_intent_service_test.dart @@ -13,6 +13,7 @@ void main() { const attachment = ViewIntentAttachment( path: '/tmp/file.jpg', type: ViewIntentAttachmentType.image, + mimeType: 'image/jpeg', localAssetId: '42', );