mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
wip
This commit is contained in:
parent
bd199f8985
commit
e1cc8f8fe1
@ -0,0 +1,127 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
package app.alextran.immich.images
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMethodCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
private object ThumbnailsPigeonUtils {
|
||||||
|
|
||||||
|
fun createConnectionError(channelName: String): FlutterError {
|
||||||
|
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
|
||||||
|
|
||||||
|
fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
return if (exception is FlutterError) {
|
||||||
|
listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
private open class ThumbnailsPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface ThumbnailApi {
|
||||||
|
fun getThumbnail(assetId: String, width: Long, height: Long, callback: (Result<ByteArray>) -> Unit)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by ThumbnailApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
ThumbnailsPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnail$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val assetIdArg = args[0] as String
|
||||||
|
val widthArg = args[1] as Long
|
||||||
|
val heightArg = args[2] as Long
|
||||||
|
api.getThumbnail(assetIdArg, widthArg, heightArg) { result: Result<ByteArray> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(ThumbnailsPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(ThumbnailsPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||||
|
class PlatformThumbnailApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||||
|
companion object {
|
||||||
|
/** The codec used by PlatformThumbnailApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
ThumbnailsPigeonCodec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getThumbnail(assetIdArg: String, widthArg: Long, heightArg: Long, callback: (Result<ByteArray?>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(assetIdArg, widthArg, heightArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
val output = it[0] as ByteArray?
|
||||||
|
callback(Result.success(output))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(ThumbnailsPigeonUtils.createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,9 @@
|
|||||||
FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
|
FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
|
||||||
FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
|
FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
|
||||||
|
FE1BEAC92E264F8400D7F138 /* Thumbhash.swift in Resources */ = {isa = PBXBuildFile; fileRef = FE1BEAC82E264F8400D7F138 /* Thumbhash.swift */; };
|
||||||
|
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
||||||
|
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -44,16 +47,6 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
dstPath = "";
|
|
||||||
dstSubfolderSpec = 10;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
name = "Embed Frameworks";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */ = {
|
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -102,6 +95,10 @@
|
|||||||
FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||||
FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
||||||
|
FE1BEAC82E264F8400D7F138 /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
||||||
|
FED3B1472E253B110030FD97 /* Flutter.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:S8QB4VV633:FLUTTER.IO LLC"; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = ../../../Flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework; sourceTree = "<group>"; };
|
||||||
|
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = "<group>"; };
|
||||||
|
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -117,8 +114,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -177,6 +172,7 @@
|
|||||||
1754452DD81DA6620E279E51 /* Frameworks */ = {
|
1754452DD81DA6620E279E51 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FED3B1472E253B110030FD97 /* Flutter.xcframework */,
|
||||||
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
|
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
|
||||||
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */,
|
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */,
|
||||||
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */,
|
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */,
|
||||||
@ -243,6 +239,7 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
FED3B1952E253E9B0030FD97 /* Images */,
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -258,6 +255,16 @@
|
|||||||
path = ShareExtension;
|
path = ShareExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
FED3B1952E253E9B0030FD97 /* Images */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FE1BEAC82E264F8400D7F138 /* Thumbhash.swift */,
|
||||||
|
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */,
|
||||||
|
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */,
|
||||||
|
);
|
||||||
|
path = Images;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -270,7 +277,6 @@
|
|||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
|
||||||
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
|
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
||||||
@ -379,6 +385,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
FE1BEAC92E264F8400D7F138 /* Thumbhash.swift in Resources */,
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
@ -523,6 +530,8 @@
|
|||||||
files = (
|
files = (
|
||||||
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
|
||||||
|
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
|
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
|
||||||
);
|
);
|
||||||
@ -651,7 +660,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
@ -660,7 +669,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.121.0;
|
MARKETING_VERSION = 1.121.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.profile;
|
||||||
PRODUCT_NAME = "Immich-Profile";
|
PRODUCT_NAME = "Immich-Profile";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@ -795,7 +804,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
@ -804,7 +813,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.121.0;
|
MARKETING_VERSION = 1.121.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.vdebug;
|
||||||
PRODUCT_NAME = "Immich-Debug";
|
PRODUCT_NAME = "Immich-Debug";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@ -825,7 +834,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
@ -834,7 +843,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.121.0;
|
MARKETING_VERSION = 1.121.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich;
|
||||||
PRODUCT_NAME = Immich;
|
PRODUCT_NAME = Immich;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@ -858,7 +867,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -875,7 +884,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.Widget;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.vdebug.Widget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
@ -901,7 +910,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -917,7 +926,7 @@
|
|||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.Widget;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.Widget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -941,7 +950,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -957,7 +966,7 @@
|
|||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.Widget;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.profile.Widget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -981,7 +990,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -998,7 +1007,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.vdebug.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@ -1025,7 +1034,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -1041,7 +1050,7 @@
|
|||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@ -1066,7 +1075,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 210;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 33MF3D8ZGA;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -1082,7 +1091,7 @@
|
|||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.mertalev.immich.profile.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -25,6 +25,7 @@ import UIKit
|
|||||||
|
|
||||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||||
NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl())
|
NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl())
|
||||||
|
ThumbnailApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ThumbnailApiImpl())
|
||||||
|
|
||||||
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||||
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
||||||
|
649
mobile/ios/Runner/Images/Thumbhash.swift
Normal file
649
mobile/ios/Runner/Images/Thumbhash.swift
Normal file
@ -0,0 +1,649 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// NOTE: Swift has an exponential-time type checker and compiling very simple
|
||||||
|
// expressions can easily take many seconds, especially when expressions involve
|
||||||
|
// numeric type constructors.
|
||||||
|
//
|
||||||
|
// This file deliberately breaks compound expressions up into separate variables
|
||||||
|
// to improve compile time even though this comes at the expense of readability.
|
||||||
|
// This is a known workaround for this deficiency in the Swift compiler.
|
||||||
|
//
|
||||||
|
// The following command is helpful when debugging Swift compile time issues:
|
||||||
|
//
|
||||||
|
// swiftc ThumbHash.swift -Xfrontend -debug-time-function-bodies
|
||||||
|
//
|
||||||
|
// These optimizations brought the compile time for this file from around 2.5
|
||||||
|
// seconds to around 250ms (10x faster).
|
||||||
|
|
||||||
|
// NOTE: Swift's debug-build performance of for-in loops over numeric ranges is
|
||||||
|
// really awful. Debug builds compile a very generic indexing iterator thing
|
||||||
|
// that makes many nested calls for every iteration, which makes debug-build
|
||||||
|
// performance crawl.
|
||||||
|
//
|
||||||
|
// This file deliberately avoids for-in loops that loop for more than a few
|
||||||
|
// times to improve debug-build run time even though this comes at the expense
|
||||||
|
// of readability. Similarly unsafe pointers are used instead of array getters
|
||||||
|
// to avoid unnecessary bounds checks, which have extra overhead in debug builds.
|
||||||
|
//
|
||||||
|
// These optimizations brought the run time to encode and decode 10 ThumbHashes
|
||||||
|
// in debug mode from 700ms to 70ms (10x faster).
|
||||||
|
|
||||||
|
func rgbaToThumbHash(w: Int, h: Int, rgba: Data) -> Data {
|
||||||
|
// Encoding an image larger than 100x100 is slow with no benefit
|
||||||
|
assert(w <= 100 && h <= 100)
|
||||||
|
assert(rgba.count == w * h * 4)
|
||||||
|
|
||||||
|
// Determine the average color
|
||||||
|
var avg_r: Float32 = 0
|
||||||
|
var avg_g: Float32 = 0
|
||||||
|
var avg_b: Float32 = 0
|
||||||
|
var avg_a: Float32 = 0
|
||||||
|
rgba.withUnsafeBytes { rgba in
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let alpha = Float32(rgba[3]) / 255
|
||||||
|
avg_r += alpha / 255 * Float32(rgba[0])
|
||||||
|
avg_g += alpha / 255 * Float32(rgba[1])
|
||||||
|
avg_b += alpha / 255 * Float32(rgba[2])
|
||||||
|
avg_a += alpha
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if avg_a > 0 {
|
||||||
|
avg_r /= avg_a
|
||||||
|
avg_g /= avg_a
|
||||||
|
avg_b /= avg_a
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasAlpha = avg_a < Float32(w * h)
|
||||||
|
let l_limit = hasAlpha ? 5 : 7 // Use fewer luminance bits if there's alpha
|
||||||
|
let imax_wh = max(w, h)
|
||||||
|
let iwl_limit = l_limit * w
|
||||||
|
let ihl_limit = l_limit * h
|
||||||
|
let fmax_wh = Float32(imax_wh)
|
||||||
|
let fwl_limit = Float32(iwl_limit)
|
||||||
|
let fhl_limit = Float32(ihl_limit)
|
||||||
|
let flx = round(fwl_limit / fmax_wh)
|
||||||
|
let fly = round(fhl_limit / fmax_wh)
|
||||||
|
var lx = Int(flx)
|
||||||
|
var ly = Int(fly)
|
||||||
|
lx = max(1, lx)
|
||||||
|
ly = max(1, ly)
|
||||||
|
var lpqa = [Float32](repeating: 0, count: w * h * 4)
|
||||||
|
|
||||||
|
// Convert the image from RGBA to LPQA (composite atop the average color)
|
||||||
|
rgba.withUnsafeBytes { rgba in
|
||||||
|
lpqa.withUnsafeMutableBytes { lpqa in
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
var lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count)
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let alpha = Float32(rgba[3]) / 255
|
||||||
|
let r = avg_r * (1 - alpha) + alpha / 255 * Float32(rgba[0])
|
||||||
|
let g = avg_g * (1 - alpha) + alpha / 255 * Float32(rgba[1])
|
||||||
|
let b = avg_b * (1 - alpha) + alpha / 255 * Float32(rgba[2])
|
||||||
|
lpqa[0] = (r + g + b) / 3
|
||||||
|
lpqa[1] = (r + g) / 2 - b
|
||||||
|
lpqa[2] = r - g
|
||||||
|
lpqa[3] = alpha
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
lpqa = lpqa.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
|
||||||
|
let encodeChannel = { (channel: UnsafePointer<Float32>, nx: Int, ny: Int) -> (Float32, [Float32], Float32) in
|
||||||
|
var dc: Float32 = 0
|
||||||
|
var ac: [Float32] = []
|
||||||
|
var scale: Float32 = 0
|
||||||
|
var fx = [Float32](repeating: 0, count: w)
|
||||||
|
fx.withUnsafeMutableBytes { fx in
|
||||||
|
let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count)
|
||||||
|
var cy = 0
|
||||||
|
while cy < ny {
|
||||||
|
var cx = 0
|
||||||
|
while cx * ny < nx * (ny - cy) {
|
||||||
|
var ptr = channel
|
||||||
|
var f: Float32 = 0
|
||||||
|
var x = 0
|
||||||
|
while x < w {
|
||||||
|
let fw = Float32(w)
|
||||||
|
let fxx = Float32(x)
|
||||||
|
let fcx = Float32(cx)
|
||||||
|
fx[x] = cos(Float32.pi / fw * fcx * (fxx + 0.5))
|
||||||
|
x += 1
|
||||||
|
}
|
||||||
|
var y = 0
|
||||||
|
while y < h {
|
||||||
|
let fh = Float32(h)
|
||||||
|
let fyy = Float32(y)
|
||||||
|
let fcy = Float32(cy)
|
||||||
|
let fy = cos(Float32.pi / fh * fcy * (fyy + 0.5))
|
||||||
|
var x = 0
|
||||||
|
while x < w {
|
||||||
|
f += ptr.pointee * fx[x] * fy
|
||||||
|
x += 1
|
||||||
|
ptr = ptr.advanced(by: 4)
|
||||||
|
}
|
||||||
|
y += 1
|
||||||
|
}
|
||||||
|
f /= Float32(w * h)
|
||||||
|
if cx > 0 || cy > 0 {
|
||||||
|
ac.append(f)
|
||||||
|
scale = max(scale, abs(f))
|
||||||
|
} else {
|
||||||
|
dc = f
|
||||||
|
}
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
cy += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scale > 0 {
|
||||||
|
let n = ac.count
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
ac[i] = 0.5 + 0.5 / scale * ac[i]
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (dc, ac, scale)
|
||||||
|
}
|
||||||
|
let (
|
||||||
|
(l_dc, l_ac, l_scale),
|
||||||
|
(p_dc, p_ac, p_scale),
|
||||||
|
(q_dc, q_ac, q_scale),
|
||||||
|
(a_dc, a_ac, a_scale)
|
||||||
|
) = lpqa.withUnsafeBytes { lpqa in
|
||||||
|
let lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count)
|
||||||
|
return (
|
||||||
|
encodeChannel(lpqa, max(3, lx), max(3, ly)),
|
||||||
|
encodeChannel(lpqa.advanced(by: 1), 3, 3),
|
||||||
|
encodeChannel(lpqa.advanced(by: 2), 3, 3),
|
||||||
|
hasAlpha ? encodeChannel(lpqa.advanced(by: 3), 5, 5) : (1, [], 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the constants
|
||||||
|
let isLandscape = w > h
|
||||||
|
let fl_dc = round(63.0 * l_dc)
|
||||||
|
let fp_dc = round(31.5 + 31.5 * p_dc)
|
||||||
|
let fq_dc = round(31.5 + 31.5 * q_dc)
|
||||||
|
let fl_scale = round(31.0 * l_scale)
|
||||||
|
let il_dc = UInt32(fl_dc)
|
||||||
|
let ip_dc = UInt32(fp_dc)
|
||||||
|
let iq_dc = UInt32(fq_dc)
|
||||||
|
let il_scale = UInt32(fl_scale)
|
||||||
|
let ihasAlpha = UInt32(hasAlpha ? 1 : 0)
|
||||||
|
let header24 = il_dc | (ip_dc << 6) | (iq_dc << 12) | (il_scale << 18) | (ihasAlpha << 23)
|
||||||
|
let fp_scale = round(63.0 * p_scale)
|
||||||
|
let fq_scale = round(63.0 * q_scale)
|
||||||
|
let ilxy = UInt16(isLandscape ? ly : lx)
|
||||||
|
let ip_scale = UInt16(fp_scale)
|
||||||
|
let iq_scale = UInt16(fq_scale)
|
||||||
|
let iisLandscape = UInt16(isLandscape ? 1 : 0)
|
||||||
|
let header16 = ilxy | (ip_scale << 3) | (iq_scale << 9) | (iisLandscape << 15)
|
||||||
|
var hash = Data(capacity: 25)
|
||||||
|
hash.append(UInt8(header24 & 255))
|
||||||
|
hash.append(UInt8((header24 >> 8) & 255))
|
||||||
|
hash.append(UInt8(header24 >> 16))
|
||||||
|
hash.append(UInt8(header16 & 255))
|
||||||
|
hash.append(UInt8(header16 >> 8))
|
||||||
|
var isOdd = false
|
||||||
|
if hasAlpha {
|
||||||
|
let fa_dc = round(15.0 * a_dc)
|
||||||
|
let fa_scale = round(15.0 * a_scale)
|
||||||
|
let ia_dc = UInt8(fa_dc)
|
||||||
|
let ia_scale = UInt8(fa_scale)
|
||||||
|
hash.append(ia_dc | (ia_scale << 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the varying factors
|
||||||
|
for ac in [l_ac, p_ac, q_ac] {
|
||||||
|
for f in ac {
|
||||||
|
let f15 = round(15.0 * f)
|
||||||
|
let i15 = UInt8(f15)
|
||||||
|
if isOdd {
|
||||||
|
hash[hash.count - 1] |= i15 << 4
|
||||||
|
} else {
|
||||||
|
hash.append(i15)
|
||||||
|
}
|
||||||
|
isOdd = !isOdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasAlpha {
|
||||||
|
for f in a_ac {
|
||||||
|
let f15 = round(15.0 * f)
|
||||||
|
let i15 = UInt8(f15)
|
||||||
|
if isOdd {
|
||||||
|
hash[hash.count - 1] |= i15 << 4
|
||||||
|
} else {
|
||||||
|
hash.append(i15)
|
||||||
|
}
|
||||||
|
isOdd = !isOdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbHashToRGBA(hash: Data) -> (Int, Int, Data) {
|
||||||
|
// Read the constants
|
||||||
|
let h0 = UInt32(hash[0])
|
||||||
|
let h1 = UInt32(hash[1])
|
||||||
|
let h2 = UInt32(hash[2])
|
||||||
|
let h3 = UInt16(hash[3])
|
||||||
|
let h4 = UInt16(hash[4])
|
||||||
|
let header24 = h0 | (h1 << 8) | (h2 << 16)
|
||||||
|
let header16 = h3 | (h4 << 8)
|
||||||
|
let il_dc = header24 & 63
|
||||||
|
let ip_dc = (header24 >> 6) & 63
|
||||||
|
let iq_dc = (header24 >> 12) & 63
|
||||||
|
var l_dc = Float32(il_dc)
|
||||||
|
var p_dc = Float32(ip_dc)
|
||||||
|
var q_dc = Float32(iq_dc)
|
||||||
|
l_dc = l_dc / 63
|
||||||
|
p_dc = p_dc / 31.5 - 1
|
||||||
|
q_dc = q_dc / 31.5 - 1
|
||||||
|
let il_scale = (header24 >> 18) & 31
|
||||||
|
var l_scale = Float32(il_scale)
|
||||||
|
l_scale = l_scale / 31
|
||||||
|
let hasAlpha = (header24 >> 23) != 0
|
||||||
|
let ip_scale = (header16 >> 3) & 63
|
||||||
|
let iq_scale = (header16 >> 9) & 63
|
||||||
|
var p_scale = Float32(ip_scale)
|
||||||
|
var q_scale = Float32(iq_scale)
|
||||||
|
p_scale = p_scale / 63
|
||||||
|
q_scale = q_scale / 63
|
||||||
|
let isLandscape = (header16 >> 15) != 0
|
||||||
|
let lx16 = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7)
|
||||||
|
let ly16 = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7)
|
||||||
|
let lx = Int(lx16)
|
||||||
|
let ly = Int(ly16)
|
||||||
|
var a_dc = Float32(1)
|
||||||
|
var a_scale = Float32(1)
|
||||||
|
if hasAlpha {
|
||||||
|
let ia_dc = hash[5] & 15
|
||||||
|
let ia_scale = hash[5] >> 4
|
||||||
|
a_dc = Float32(ia_dc)
|
||||||
|
a_scale = Float32(ia_scale)
|
||||||
|
a_dc /= 15
|
||||||
|
a_scale /= 15
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
||||||
|
let ac_start = hasAlpha ? 6 : 5
|
||||||
|
var ac_index = 0
|
||||||
|
let decodeChannel = { (nx: Int, ny: Int, scale: Float32) -> [Float32] in
|
||||||
|
var ac: [Float32] = []
|
||||||
|
for cy in 0 ..< ny {
|
||||||
|
var cx = cy > 0 ? 0 : 1
|
||||||
|
while cx * ny < nx * (ny - cy) {
|
||||||
|
let iac = (hash[ac_start + (ac_index >> 1)] >> ((ac_index & 1) << 2)) & 15;
|
||||||
|
var fac = Float32(iac)
|
||||||
|
fac = (fac / 7.5 - 1) * scale
|
||||||
|
ac.append(fac)
|
||||||
|
ac_index += 1
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
let l_ac = decodeChannel(lx, ly, l_scale)
|
||||||
|
let p_ac = decodeChannel(3, 3, p_scale * 1.25)
|
||||||
|
let q_ac = decodeChannel(3, 3, q_scale * 1.25)
|
||||||
|
let a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : []
|
||||||
|
|
||||||
|
// Decode using the DCT into RGB
|
||||||
|
let ratio = thumbHashToApproximateAspectRatio(hash: hash)
|
||||||
|
let fw = round(ratio > 1 ? 32 : 32 * ratio)
|
||||||
|
let fh = round(ratio > 1 ? 32 / ratio : 32)
|
||||||
|
let w = Int(fw)
|
||||||
|
let h = Int(fh)
|
||||||
|
var rgba = Data(count: w * h * 4)
|
||||||
|
let cx_stop = max(lx, hasAlpha ? 5 : 3)
|
||||||
|
let cy_stop = max(ly, hasAlpha ? 5 : 3)
|
||||||
|
var fx = [Float32](repeating: 0, count: cx_stop)
|
||||||
|
var fy = [Float32](repeating: 0, count: cy_stop)
|
||||||
|
fx.withUnsafeMutableBytes { fx in
|
||||||
|
let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count)
|
||||||
|
fy.withUnsafeMutableBytes { fy in
|
||||||
|
let fy = fy.baseAddress!.bindMemory(to: Float32.self, capacity: fy.count)
|
||||||
|
rgba.withUnsafeMutableBytes { rgba in
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
var y = 0
|
||||||
|
while y < h {
|
||||||
|
var x = 0
|
||||||
|
while x < w {
|
||||||
|
var l = l_dc
|
||||||
|
var p = p_dc
|
||||||
|
var q = q_dc
|
||||||
|
var a = a_dc
|
||||||
|
|
||||||
|
// Precompute the coefficients
|
||||||
|
var cx = 0
|
||||||
|
while cx < cx_stop {
|
||||||
|
let fw = Float32(w)
|
||||||
|
let fxx = Float32(x)
|
||||||
|
let fcx = Float32(cx)
|
||||||
|
fx[cx] = cos(Float32.pi / fw * (fxx + 0.5) * fcx)
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
var cy = 0
|
||||||
|
while cy < cy_stop {
|
||||||
|
let fh = Float32(h)
|
||||||
|
let fyy = Float32(y)
|
||||||
|
let fcy = Float32(cy)
|
||||||
|
fy[cy] = cos(Float32.pi / fh * (fyy + 0.5) * fcy)
|
||||||
|
cy += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode L
|
||||||
|
var j = 0
|
||||||
|
cy = 0
|
||||||
|
while cy < ly {
|
||||||
|
var cx = cy > 0 ? 0 : 1
|
||||||
|
let fy2 = fy[cy] * 2
|
||||||
|
while cx * ly < lx * (ly - cy) {
|
||||||
|
l += l_ac[j] * fx[cx] * fy2
|
||||||
|
j += 1
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
cy += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode P and Q
|
||||||
|
j = 0
|
||||||
|
cy = 0
|
||||||
|
while cy < 3 {
|
||||||
|
var cx = cy > 0 ? 0 : 1
|
||||||
|
let fy2 = fy[cy] * 2
|
||||||
|
while cx < 3 - cy {
|
||||||
|
let f = fx[cx] * fy2
|
||||||
|
p += p_ac[j] * f
|
||||||
|
q += q_ac[j] * f
|
||||||
|
j += 1
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
cy += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode A
|
||||||
|
if hasAlpha {
|
||||||
|
j = 0
|
||||||
|
cy = 0
|
||||||
|
while cy < 5 {
|
||||||
|
var cx = cy > 0 ? 0 : 1
|
||||||
|
let fy2 = fy[cy] * 2
|
||||||
|
while cx < 5 - cy {
|
||||||
|
a += a_ac[j] * fx[cx] * fy2
|
||||||
|
j += 1
|
||||||
|
cx += 1
|
||||||
|
}
|
||||||
|
cy += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to RGB
|
||||||
|
var b = l - 2 / 3 * p
|
||||||
|
var r = (3 * l - b + q) / 2
|
||||||
|
var g = r - q
|
||||||
|
r = max(0, 255 * min(1, r))
|
||||||
|
g = max(0, 255 * min(1, g))
|
||||||
|
b = max(0, 255 * min(1, b))
|
||||||
|
a = max(0, 255 * min(1, a))
|
||||||
|
rgba[0] = UInt8(r)
|
||||||
|
rgba[1] = UInt8(g)
|
||||||
|
rgba[2] = UInt8(b)
|
||||||
|
rgba[3] = UInt8(a)
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
x += 1
|
||||||
|
}
|
||||||
|
y += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (w, h, rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbHashToAverageRGBA(hash: Data) -> (Float32, Float32, Float32, Float32) {
|
||||||
|
let h0 = UInt32(hash[0])
|
||||||
|
let h1 = UInt32(hash[1])
|
||||||
|
let h2 = UInt32(hash[2])
|
||||||
|
let header = h0 | (h1 << 8) | (h2 << 16)
|
||||||
|
let il = header & 63
|
||||||
|
let ip = (header >> 6) & 63
|
||||||
|
let iq = (header >> 12) & 63
|
||||||
|
var l = Float32(il)
|
||||||
|
var p = Float32(ip)
|
||||||
|
var q = Float32(iq)
|
||||||
|
l = l / 63
|
||||||
|
p = p / 31.5 - 1
|
||||||
|
q = q / 31.5 - 1
|
||||||
|
let hasAlpha = (header >> 23) != 0
|
||||||
|
var a = Float32(1)
|
||||||
|
if hasAlpha {
|
||||||
|
let ia = hash[5] & 15
|
||||||
|
a = Float32(ia)
|
||||||
|
a = a / 15
|
||||||
|
}
|
||||||
|
let b = l - 2 / 3 * p
|
||||||
|
let r = (3 * l - b + q) / 2
|
||||||
|
let g = r - q
|
||||||
|
return (
|
||||||
|
max(0, min(1, r)),
|
||||||
|
max(0, min(1, g)),
|
||||||
|
max(0, min(1, b)),
|
||||||
|
a
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbHashToApproximateAspectRatio(hash: Data) -> Float32 {
|
||||||
|
let header = hash[3]
|
||||||
|
let hasAlpha = (hash[2] & 0x80) != 0
|
||||||
|
let isLandscape = (hash[4] & 0x80) != 0
|
||||||
|
let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7
|
||||||
|
let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7
|
||||||
|
return Float32(lx) / Float32(ly)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
func imageToThumbHash(image: NSImage) -> Data {
|
||||||
|
let size = image.size
|
||||||
|
let fw = round(100 * size.width / max(size.width, size.height))
|
||||||
|
let fh = round(100 * size.height / max(size.width, size.height))
|
||||||
|
let w = Int(fw)
|
||||||
|
let h = Int(fh)
|
||||||
|
var rgba = Data(count: w * h * 4)
|
||||||
|
rgba.withUnsafeMutableBytes { rgba in
|
||||||
|
var rect = NSRect(x: 0, y: 0, width: w, height: h)
|
||||||
|
if
|
||||||
|
let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil),
|
||||||
|
let space = (image.representations[0] as? NSBitmapImageRep)?.colorSpace.cgColorSpace,
|
||||||
|
let context = CGContext(
|
||||||
|
data: rgba.baseAddress,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: w * 4,
|
||||||
|
space: space,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||||
|
)
|
||||||
|
{
|
||||||
|
context.draw(cgImage, in: rect)
|
||||||
|
|
||||||
|
// Convert from premultiplied alpha to unpremultiplied alpha
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let a = UInt16(rgba[3])
|
||||||
|
if a > 0 && a < 255 {
|
||||||
|
var r = UInt16(rgba[0])
|
||||||
|
var g = UInt16(rgba[1])
|
||||||
|
var b = UInt16(rgba[2])
|
||||||
|
r = min(255, r * 255 / a)
|
||||||
|
g = min(255, g * 255 / a)
|
||||||
|
b = min(255, b * 255 / a)
|
||||||
|
rgba[0] = UInt8(r)
|
||||||
|
rgba[1] = UInt8(g)
|
||||||
|
rgba[2] = UInt8(b)
|
||||||
|
}
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgbaToThumbHash(w: w, h: h, rgba: rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbHashToImage(hash: Data) -> NSImage {
|
||||||
|
let (w, h, rgba) = thumbHashToRGBA(hash: hash)
|
||||||
|
let bitmap = NSBitmapImageRep(
|
||||||
|
bitmapDataPlanes: nil,
|
||||||
|
pixelsWide: w,
|
||||||
|
pixelsHigh: h,
|
||||||
|
bitsPerSample: 8,
|
||||||
|
samplesPerPixel: 4,
|
||||||
|
hasAlpha: true,
|
||||||
|
isPlanar: false,
|
||||||
|
colorSpaceName: .deviceRGB,
|
||||||
|
bytesPerRow: w * 4,
|
||||||
|
bitsPerPixel: 32
|
||||||
|
)!
|
||||||
|
rgba.withUnsafeBytes { rgba in
|
||||||
|
// Convert from unpremultiplied alpha to premultiplied alpha
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
var to = bitmap.bitmapData!
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let a = rgba[3]
|
||||||
|
if a == 255 {
|
||||||
|
to[0] = rgba[0]
|
||||||
|
to[1] = rgba[1]
|
||||||
|
to[2] = rgba[2]
|
||||||
|
} else {
|
||||||
|
var r = UInt16(rgba[0])
|
||||||
|
var g = UInt16(rgba[1])
|
||||||
|
var b = UInt16(rgba[2])
|
||||||
|
let a = UInt16(a)
|
||||||
|
r = min(255, r * a / 255)
|
||||||
|
g = min(255, g * a / 255)
|
||||||
|
b = min(255, b * a / 255)
|
||||||
|
to[0] = UInt8(r)
|
||||||
|
to[1] = UInt8(g)
|
||||||
|
to[2] = UInt8(b)
|
||||||
|
}
|
||||||
|
to[3] = a
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
to = to.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let image = NSImage(size: NSSize(width: w, height: h))
|
||||||
|
image.addRepresentation(bitmap)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
func imageToThumbHash(image: UIImage) -> Data {
|
||||||
|
let size = image.size
|
||||||
|
let w = Int(round(100 * size.width / max(size.width, size.height)))
|
||||||
|
let h = Int(round(100 * size.height / max(size.width, size.height)))
|
||||||
|
var rgba = Data(count: w * h * 4)
|
||||||
|
rgba.withUnsafeMutableBytes { rgba in
|
||||||
|
if
|
||||||
|
let space = image.cgImage?.colorSpace,
|
||||||
|
let context = CGContext(
|
||||||
|
data: rgba.baseAddress,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: w * 4,
|
||||||
|
space: space,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// EXIF orientation only works if you draw the UIImage, not the CGImage
|
||||||
|
context.concatenate(CGAffineTransform(1, 0, 0, -1, 0, CGFloat(h)))
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
image.draw(in: CGRect(x: 0, y: 0, width: w, height: h))
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
|
||||||
|
// Convert from premultiplied alpha to unpremultiplied alpha
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let a = UInt16(rgba[3])
|
||||||
|
if a > 0 && a < 255 {
|
||||||
|
var r = UInt16(rgba[0])
|
||||||
|
var g = UInt16(rgba[1])
|
||||||
|
var b = UInt16(rgba[2])
|
||||||
|
r = min(255, r * 255 / a)
|
||||||
|
g = min(255, g * 255 / a)
|
||||||
|
b = min(255, b * 255 / a)
|
||||||
|
rgba[0] = UInt8(r)
|
||||||
|
rgba[1] = UInt8(g)
|
||||||
|
rgba[2] = UInt8(b)
|
||||||
|
}
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgbaToThumbHash(w: w, h: h, rgba: rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbHashToImage(hash: Data) -> UIImage {
|
||||||
|
var (w, h, rgba) = thumbHashToRGBA(hash: hash)
|
||||||
|
rgba.withUnsafeMutableBytes { rgba in
|
||||||
|
// Convert from unpremultiplied alpha to premultiplied alpha
|
||||||
|
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
|
||||||
|
let n = w * h
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let a = UInt16(rgba[3])
|
||||||
|
if a < 255 {
|
||||||
|
var r = UInt16(rgba[0])
|
||||||
|
var g = UInt16(rgba[1])
|
||||||
|
var b = UInt16(rgba[2])
|
||||||
|
r = min(255, r * a / 255)
|
||||||
|
g = min(255, g * a / 255)
|
||||||
|
b = min(255, b * a / 255)
|
||||||
|
rgba[0] = UInt8(r)
|
||||||
|
rgba[1] = UInt8(g)
|
||||||
|
rgba[2] = UInt8(b)
|
||||||
|
}
|
||||||
|
rgba = rgba.advanced(by: 4)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let image = CGImage(
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 32,
|
||||||
|
bytesPerRow: w * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||||
|
provider: CGDataProvider(data: rgba as CFData)!,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: true,
|
||||||
|
intent: .perceptual
|
||||||
|
)
|
||||||
|
return UIImage(cgImage: image!)
|
||||||
|
}
|
||||||
|
#endif
|
140
mobile/ios/Runner/Images/Thumbnails.g.swift
Normal file
140
mobile/ios/Runner/Images/Thumbnails.g.swift
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.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 createConnectionError(withChannelName channelName: String) -> PigeonError {
|
||||||
|
return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isNullish(_ value: Any?) -> Bool {
|
||||||
|
return value is NSNull || value == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
return value as! T?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class ThumbnailsPigeonCodecReader: FlutterStandardReader {
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ThumbnailsPigeonCodecWriter: FlutterStandardWriter {
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ThumbnailsPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||||
|
override func reader(with data: Data) -> FlutterStandardReader {
|
||||||
|
return ThumbnailsPigeonCodecReader(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||||
|
return ThumbnailsPigeonCodecWriter(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThumbnailsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||||
|
static let shared = ThumbnailsPigeonCodec(readerWriter: ThumbnailsPigeonCodecReaderWriter())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
protocol ThumbnailApi {
|
||||||
|
func getThumbnail(assetId: String, width: Int64, height: Int64, completion: @escaping (Result<FlutterStandardTypedData, Error>) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class ThumbnailApiSetup {
|
||||||
|
static var codec: FlutterStandardMessageCodec { ThumbnailsPigeonCodec.shared }
|
||||||
|
/// 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 getThumbnailChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnail\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
getThumbnailChannel.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.getThumbnail(assetId: assetIdArg, width: widthArg, height: heightArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getThumbnailChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
|
protocol PlatformThumbnailApiProtocol {
|
||||||
|
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void)
|
||||||
|
}
|
||||||
|
class PlatformThumbnailApi: PlatformThumbnailApiProtocol {
|
||||||
|
private let binaryMessenger: FlutterBinaryMessenger
|
||||||
|
private let messageChannelSuffix: String
|
||||||
|
init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
|
||||||
|
self.binaryMessenger = binaryMessenger
|
||||||
|
self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
|
}
|
||||||
|
var codec: ThumbnailsPigeonCodec {
|
||||||
|
return ThumbnailsPigeonCodec.shared
|
||||||
|
}
|
||||||
|
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void) {
|
||||||
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail\(messageChannelSuffix)"
|
||||||
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([assetIdArg, widthArg, heightArg] as [Any?]) { response in
|
||||||
|
guard let listResponse = response as? [Any?] else {
|
||||||
|
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if listResponse.count > 1 {
|
||||||
|
let code: String = listResponse[0] as! String
|
||||||
|
let message: String? = nilOrValue(listResponse[1])
|
||||||
|
let details: String? = nilOrValue(listResponse[2])
|
||||||
|
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
||||||
|
} else {
|
||||||
|
let result: FlutterStandardTypedData? = nilOrValue(listResponse[0])
|
||||||
|
completion(.success(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
151
mobile/ios/Runner/Images/ThumbnailsImpl.swift
Normal file
151
mobile/ios/Runner/Images/ThumbnailsImpl.swift
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import CryptoKit
|
||||||
|
import Flutter
|
||||||
|
import Photos
|
||||||
|
import MobileCoreServices
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/55839062
|
||||||
|
extension UIImage {
|
||||||
|
func toData (options: NSDictionary?, type: ImageType) -> Data? {
|
||||||
|
guard cgImage != nil else { return nil }
|
||||||
|
return toData(options: options, type: type.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// about properties: https://developer.apple.com/documentation/imageio/1464962-cgimagedestinationaddimage
|
||||||
|
func toData (options: NSDictionary?, type: CFString) -> Data? {
|
||||||
|
guard let cgImage = cgImage else { return nil }
|
||||||
|
return autoreleasepool { () -> Data? in
|
||||||
|
let data = NSMutableData()
|
||||||
|
guard let imageDestination = CGImageDestinationCreateWithData(data as CFMutableData, type, 1, nil) else { return nil }
|
||||||
|
CGImageDestinationAddImage(imageDestination, cgImage, options)
|
||||||
|
CGImageDestinationFinalize(imageDestination)
|
||||||
|
return data as Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ImageType {
|
||||||
|
case image // abstract image data
|
||||||
|
case jpeg // JPEG image
|
||||||
|
case jpeg2000 // JPEG-2000 image
|
||||||
|
case tiff // TIFF image
|
||||||
|
case pict // Quickdraw PICT format
|
||||||
|
case gif // GIF image
|
||||||
|
case png // PNG image
|
||||||
|
case quickTimeImage // QuickTime image format (OSType 'qtif')
|
||||||
|
case appleICNS // Apple icon data
|
||||||
|
case bmp // Windows bitmap
|
||||||
|
case ico // Windows icon data
|
||||||
|
case rawImage // base type for raw image data (.raw)
|
||||||
|
case scalableVectorGraphics // SVG image
|
||||||
|
case livePhoto // Live Photo
|
||||||
|
|
||||||
|
var value: CFString {
|
||||||
|
switch self {
|
||||||
|
case .image: return kUTTypeImage
|
||||||
|
case .jpeg: return kUTTypeJPEG
|
||||||
|
case .jpeg2000: return kUTTypeJPEG2000
|
||||||
|
case .tiff: return kUTTypeTIFF
|
||||||
|
case .pict: return kUTTypePICT
|
||||||
|
case .gif: return kUTTypeGIF
|
||||||
|
case .png: return kUTTypePNG
|
||||||
|
case .quickTimeImage: return kUTTypeQuickTimeImage
|
||||||
|
case .appleICNS: return kUTTypeAppleICNS
|
||||||
|
case .bmp: return kUTTypeBMP
|
||||||
|
case .ico: return kUTTypeICO
|
||||||
|
case .rawImage: return kUTTypeRawImage
|
||||||
|
case .scalableVectorGraphics: return kUTTypeScalableVectorGraphics
|
||||||
|
case .livePhoto: return kUTTypeLivePhoto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThumbnailApiImpl: ThumbnailApi {
|
||||||
|
private static let cacheManager = PHImageManager.default()
|
||||||
|
private static let fetchOptions = {
|
||||||
|
let fetchOptions = PHFetchOptions()
|
||||||
|
fetchOptions.fetchLimit = 1
|
||||||
|
return fetchOptions
|
||||||
|
}()
|
||||||
|
private static let requestOptions = {
|
||||||
|
let requestOptions = PHImageRequestOptions()
|
||||||
|
requestOptions.isNetworkAccessAllowed = true
|
||||||
|
requestOptions.deliveryMode = .highQualityFormat
|
||||||
|
requestOptions.resizeMode = .exact
|
||||||
|
requestOptions.isSynchronous = true
|
||||||
|
requestOptions.version = .current
|
||||||
|
return requestOptions
|
||||||
|
}()
|
||||||
|
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
|
||||||
|
private static let imageCache = NSCache<NSString, FlutterStandardTypedData>()
|
||||||
|
|
||||||
|
func requestThumbnail(
|
||||||
|
assetId: String,
|
||||||
|
width: Int64,
|
||||||
|
height: Int64,
|
||||||
|
completion: @escaping (Result<Int32, Error>) -> Void
|
||||||
|
) {
|
||||||
|
Self.processingQueue.async {
|
||||||
|
do {
|
||||||
|
let asset = try self.getAsset(assetId: assetId)
|
||||||
|
|
||||||
|
let requestId = Self.cacheManager.requestImage(
|
||||||
|
for: asset,
|
||||||
|
targetSize: CGSize(width: Double(width), height: Double(height)),
|
||||||
|
contentMode: .aspectFill,
|
||||||
|
options: Self.requestOptions,
|
||||||
|
resultHandler: { (image, info) -> Void in
|
||||||
|
guard let data = image?.toData(options: nil, type: .bmp) else { return }
|
||||||
|
Self.imageCache.setObject(FlutterStandardTypedData(bytes: data), forKey: assetId as NSString)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
completion(.success(requestId))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(PigeonError(code: "", message: "Could not get asset data", details: nil)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getThumbnail(assetId assetIdArg: String, width widthArg: Int64, height heightArg: Int64, completion: @escaping (Result<FlutterStandardTypedData?, PigeonError>) -> Void) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendThumbnail(
|
||||||
|
assetId: String,
|
||||||
|
width: Int64,
|
||||||
|
height: Int64,
|
||||||
|
completion: @escaping (Result<FlutterStandardTypedData, Error>) -> Void
|
||||||
|
) {
|
||||||
|
Self.processingQueue.async {
|
||||||
|
do {
|
||||||
|
let asset = try self.getAsset(assetId: assetId)
|
||||||
|
|
||||||
|
Self.cacheManager.requestImage(
|
||||||
|
for: asset,
|
||||||
|
targetSize: CGSize(width: Double(width), height: Double(height)),
|
||||||
|
contentMode: .aspectFill,
|
||||||
|
options: Self.requestOptions,
|
||||||
|
resultHandler: { (image, info) -> Void in
|
||||||
|
guard let data = image?.toData(options: nil, type: .bmp) else { return }
|
||||||
|
completion(.success(FlutterStandardTypedData(bytes: data)))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
completion(.failure(PigeonError(code: "", message: "Could not get asset data", details: nil)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel(requestId: Int32) {
|
||||||
|
Self.cacheManager.cancelImageRequest(requestId as PHImageRequestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAsset(assetId: String) throws -> PHAsset {
|
||||||
|
guard
|
||||||
|
let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions)
|
||||||
|
.firstObject
|
||||||
|
else {
|
||||||
|
throw PigeonError(code: "", message: "Could not fetch asset", details: nil)
|
||||||
|
}
|
||||||
|
return asset
|
||||||
|
}
|
||||||
|
}
|
@ -1,187 +1,187 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeName</key>
|
<key>CFBundleTypeName</key>
|
||||||
<string>ShareHandler</string>
|
<string>ShareHandler</string>
|
||||||
<key>LSHandlerRank</key>
|
<key>LSHandlerRank</key>
|
||||||
<string>Alternate</string>
|
<string>Alternate</string>
|
||||||
<key>LSItemContentTypes</key>
|
<key>LSItemContentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>public.file-url</string>
|
<string>public.file-url</string>
|
||||||
<string>public.image</string>
|
<string>public.image</string>
|
||||||
<string>public.text</string>
|
<string>public.text</string>
|
||||||
<string>public.movie</string>
|
<string>public.movie</string>
|
||||||
<string>public.url</string>
|
<string>public.url</string>
|
||||||
<string>public.data</string>
|
<string>public.data</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<string>ar</string>
|
<string>ar</string>
|
||||||
<string>ca</string>
|
<string>ca</string>
|
||||||
<string>cs</string>
|
<string>cs</string>
|
||||||
<string>da</string>
|
<string>da</string>
|
||||||
<string>de</string>
|
<string>de</string>
|
||||||
<string>es</string>
|
<string>es</string>
|
||||||
<string>fi</string>
|
<string>fi</string>
|
||||||
<string>fr</string>
|
<string>fr</string>
|
||||||
<string>he</string>
|
<string>he</string>
|
||||||
<string>hi</string>
|
<string>hi</string>
|
||||||
<string>hu</string>
|
<string>hu</string>
|
||||||
<string>it</string>
|
<string>it</string>
|
||||||
<string>ja</string>
|
<string>ja</string>
|
||||||
<string>ko</string>
|
<string>ko</string>
|
||||||
<string>lv</string>
|
<string>lv</string>
|
||||||
<string>mn</string>
|
<string>mn</string>
|
||||||
<string>nb</string>
|
<string>nb</string>
|
||||||
<string>nl</string>
|
<string>nl</string>
|
||||||
<string>pl</string>
|
<string>pl</string>
|
||||||
<string>pt</string>
|
<string>pt</string>
|
||||||
<string>ro</string>
|
<string>ro</string>
|
||||||
<string>ru</string>
|
<string>ru</string>
|
||||||
<string>sk</string>
|
<string>sk</string>
|
||||||
<string>sl</string>
|
<string>sl</string>
|
||||||
<string>sr</string>
|
<string>sr</string>
|
||||||
<string>sv</string>
|
<string>sv</string>
|
||||||
<string>th</string>
|
<string>th</string>
|
||||||
<string>uk</string>
|
<string>uk</string>
|
||||||
<string>vi</string>
|
<string>vi</string>
|
||||||
<string>zh</string>
|
<string>zh</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>immich_mobile</string>
|
<string>immich_mobile</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.135.1</string>
|
<string>1.135.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>Share Extension</string>
|
<string>Share Extension</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>Deep Link</string>
|
<string>Deep Link</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>immich</string>
|
<string>immich</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>210</string>
|
<string>210</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<string>No</string>
|
<string>No</string>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true />
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_googlecast._tcp</string>
|
<string>_googlecast._tcp</string>
|
||||||
<string>_CC1AD845._googlecast._tcp</string>
|
<string>_CC1AD845._googlecast._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We need local network permission to connect to the local server using IP address and
|
||||||
<key>NSLocationUsageDescription</key>
|
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>We need to manage backup your photos album</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>We need to manage backup your photos album</string>
|
|
||||||
<key>NSUserActivityTypes</key>
|
|
||||||
<array>
|
|
||||||
<string>INSendMessageIntent</string>
|
|
||||||
</array>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true />
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>fetch</string>
|
|
||||||
<string>processing</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UIStatusBarHidden</key>
|
|
||||||
<false />
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<true />
|
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
|
||||||
<true />
|
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
|
||||||
<string>We need local network permission to connect to the local server using IP address and
|
|
||||||
allow the casting feature to work</string>
|
allow the casting feature to work</string>
|
||||||
</dict>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
</plist>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
|
<key>NSLocationUsageDescription</key>
|
||||||
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>We need to manage backup your photos album</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>We need to manage backup your photos album</string>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
@ -9,8 +9,6 @@
|
|||||||
<key>com.apple.developer.networking.wifi-info</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array/>
|
||||||
<string>group.app.immich.share</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -11,8 +11,6 @@
|
|||||||
<key>com.apple.developer.networking.wifi-info</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array/>
|
||||||
<string>group.app.immich.share</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>IntentsSupported</key>
|
<key>IntentsSupported</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
|
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
|
||||||
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|
||||||
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
|
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
|
||||||
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
|
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
|
||||||
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
|
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
|
||||||
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0
|
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0
|
||||||
).@count > 0 </string>
|
).@count > 0 </string>
|
||||||
<key>PHSupportedMediaTypes</key>
|
<key>PHSupportedMediaTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Video</string>
|
<string>Video</string>
|
||||||
<string>Image</string>
|
<string>Image</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtensionMainStoryboard</key>
|
<key>NSExtensionMainStoryboard</key>
|
||||||
<string>MainInterface</string>
|
<string>MainInterface</string>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
<string>com.apple.share-services</string>
|
<string>com.apple.share-services</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array/>
|
||||||
<string>group.app.immich.share</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array/>
|
||||||
<string>group.app.immich.share</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
150
mobile/lib/platform/thumbnail_api.g.dart
generated
Normal file
150
mobile/lib/platform/thumbnail_api.g.dart
generated
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
PlatformException _createConnectionError(String channelName) {
|
||||||
|
return PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Object?> wrapResponse(
|
||||||
|
{Object? result, PlatformException? error, bool empty = false}) {
|
||||||
|
if (empty) {
|
||||||
|
return <Object?>[];
|
||||||
|
}
|
||||||
|
if (error == null) {
|
||||||
|
return <Object?>[result];
|
||||||
|
}
|
||||||
|
return <Object?>[error.code, error.message, error.details];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' : '';
|
||||||
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<Uint8List> getThumbnail(
|
||||||
|
String assetId, {
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
}) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbnail$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture =
|
||||||
|
pigeonVar_channel.send(<Object?>[assetId, width, height]);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as Uint8List?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PlatformThumbnailApi {
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
Future<Uint8List?> getThumbnail(String assetId, int width, int height);
|
||||||
|
|
||||||
|
static void setUp(
|
||||||
|
PlatformThumbnailApi? api, {
|
||||||
|
BinaryMessenger? binaryMessenger,
|
||||||
|
String messageChannelSuffix = '',
|
||||||
|
}) {
|
||||||
|
messageChannelSuffix =
|
||||||
|
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
{
|
||||||
|
final BasicMessageChannel<
|
||||||
|
Object?> pigeonVar_channel = BasicMessageChannel<
|
||||||
|
Object?>(
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail$messageChannelSuffix',
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: binaryMessenger);
|
||||||
|
if (api == null) {
|
||||||
|
pigeonVar_channel.setMessageHandler(null);
|
||||||
|
} else {
|
||||||
|
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||||
|
assert(message != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null.');
|
||||||
|
final List<Object?> args = (message as List<Object?>?)!;
|
||||||
|
final String? arg_assetId = (args[0] as String?);
|
||||||
|
assert(arg_assetId != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null String.');
|
||||||
|
final int? arg_width = (args[1] as int?);
|
||||||
|
assert(arg_width != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null int.');
|
||||||
|
final int? arg_height = (args[2] as int?);
|
||||||
|
assert(arg_height != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.PlatformThumbnailApi.getThumbnail was null, expected non-null int.');
|
||||||
|
try {
|
||||||
|
final Uint8List? output =
|
||||||
|
await api.getThumbnail(arg_assetId!, arg_width!, arg_height!);
|
||||||
|
return wrapResponse(result: output);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
return wrapResponse(error: e);
|
||||||
|
} catch (e) {
|
||||||
|
return wrapResponse(
|
||||||
|
error: PlatformException(code: 'error', message: e.toString()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -119,7 +119,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
final asset = selectedAssets.elementAt(index);
|
final asset = selectedAssets.elementAt(index);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onBackgroundTapped,
|
onTap: onBackgroundTapped,
|
||||||
child: Thumbnail(asset: asset),
|
child: Thumbnail.fromBaseAsset(asset: asset),
|
||||||
);
|
);
|
||||||
}, childCount: selectedAssets.length),
|
}, childCount: selectedAssets.length),
|
||||||
),
|
),
|
||||||
|
@ -482,7 +482,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
|
child: Thumbnail.fromBaseAsset(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,7 +532,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
|
child: Thumbnail.fromBaseAsset(asset: asset, fit: BoxFit.contain, size: size),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/widgets/common/thumbhash.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
class FullImage extends StatelessWidget {
|
class FullImage extends StatelessWidget {
|
||||||
@ -9,7 +9,7 @@ class FullImage extends StatelessWidget {
|
|||||||
this.asset, {
|
this.asset, {
|
||||||
required this.size,
|
required this.size,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.placeholder = const Thumbhash(),
|
this.placeholder = const Thumbnail(),
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,53 +1,402 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
||||||
|
|
||||||
class Thumbnail extends StatelessWidget {
|
final log = Logger('ThumbnailWidget');
|
||||||
const Thumbnail({this.asset, this.remoteId, this.size = const Size.square(256), this.fit = BoxFit.cover, super.key})
|
|
||||||
: assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
|
||||||
|
|
||||||
final BaseAsset? asset;
|
class Thumbnail extends StatefulWidget {
|
||||||
final String? remoteId;
|
|
||||||
final Size size;
|
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
final Size size;
|
||||||
|
final String? blurhash;
|
||||||
|
final String? localId;
|
||||||
|
final String? remoteId;
|
||||||
|
final bool thumbhashOnly;
|
||||||
|
|
||||||
|
const Thumbnail({
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.size = const Size.square(256),
|
||||||
|
this.blurhash,
|
||||||
|
this.localId,
|
||||||
|
this.remoteId,
|
||||||
|
this.thumbhashOnly = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
Thumbnail.fromAsset({
|
||||||
|
required Asset asset,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.size = const Size.square(256),
|
||||||
|
this.thumbhashOnly = false,
|
||||||
|
super.key,
|
||||||
|
}) : blurhash = asset.thumbhash,
|
||||||
|
localId = asset.localId,
|
||||||
|
remoteId = asset.remoteId;
|
||||||
|
|
||||||
|
Thumbnail.fromBaseAsset({
|
||||||
|
required BaseAsset? asset,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.size = const Size.square(256),
|
||||||
|
this.thumbhashOnly = false,
|
||||||
|
super.key,
|
||||||
|
}) : blurhash = switch (asset) {
|
||||||
|
RemoteAsset() => asset.thumbHash,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
localId = switch (asset) {
|
||||||
|
RemoteAsset() => asset.localId,
|
||||||
|
LocalAsset() => asset.id,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
remoteId = switch (asset) {
|
||||||
|
RemoteAsset() => asset.id,
|
||||||
|
LocalAsset() => asset.remoteId,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Thumbnail> createState() => _ThumbnailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThumbnailState extends State<Thumbnail> {
|
||||||
|
ui.Image? _image;
|
||||||
|
|
||||||
|
static final _gradientCache = <ColorScheme, Gradient>{};
|
||||||
|
static final _imageCache = ThumbnailImageCacheManager();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_decode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(Thumbnail oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.blurhash != widget.blurhash ||
|
||||||
|
oldWidget.localId != widget.localId ||
|
||||||
|
oldWidget.remoteId != widget.remoteId ||
|
||||||
|
oldWidget.thumbhashOnly != widget.thumbhashOnly) {
|
||||||
|
_decode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _decode() async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbhashOnly = widget.thumbhashOnly;
|
||||||
|
final blurhash = widget.blurhash;
|
||||||
|
final imageFuture = thumbhashOnly ? Future.value(null) : _decodeFromFile();
|
||||||
|
|
||||||
|
if (blurhash != null) {
|
||||||
|
final image = thumbhash.thumbHashToRGBA(base64.decode(blurhash));
|
||||||
|
try {
|
||||||
|
await _decodeThumbhash(
|
||||||
|
await ImmutableBuffer.fromUint8List(image.rgba),
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log.info('Error decoding thumbhash for ${widget.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted || thumbhashOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final image = await imageFuture;
|
||||||
|
if (!mounted || image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_image?.dispose();
|
||||||
|
setState(() {
|
||||||
|
_image = image;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log.info('Error decoding thumbnail: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _decodeThumbhash(
|
||||||
|
ImmutableBuffer buffer,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
) async {
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final descriptor = ImageDescriptor.raw(
|
||||||
|
buffer,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
pixelFormat: PixelFormat.rgba8888,
|
||||||
|
);
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final codec = await descriptor.instantiateCodec(
|
||||||
|
targetWidth: width,
|
||||||
|
targetHeight: height,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final frame = (await codec.getNextFrame()).image;
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
if (!mounted) {
|
||||||
|
frame.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_image = frame;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Image?> _decodeFromFile() async {
|
||||||
|
final buffer = await _getFile();
|
||||||
|
if (buffer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
final thumb = await _decodeThumbnail(buffer, 256);
|
||||||
|
stopwatch.stop();
|
||||||
|
return thumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Image?> _decodeThumbnail(ImmutableBuffer buffer, int height) async {
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final descriptor = await ImageDescriptor.encoded(buffer);
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final codec = await descriptor.instantiateCodec(targetHeight: height);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final frame = (await codec.getNextFrame()).image;
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
codec.dispose();
|
||||||
|
if (!mounted) {
|
||||||
|
frame.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ImmutableBuffer?> _getFile() async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
final localId = widget.localId;
|
||||||
|
if (localId != null) {
|
||||||
|
try {
|
||||||
|
final data =
|
||||||
|
await thumbnailApi.getThumbnail(localId, width: 256, height: 256);
|
||||||
|
stopwatch.stop();
|
||||||
|
log.info(
|
||||||
|
'Retrieved local image $localId in ${stopwatch.elapsedMilliseconds.toStringAsFixed(2)} ms',
|
||||||
|
);
|
||||||
|
return ImmutableBuffer.fromUint8List(data);
|
||||||
|
} catch (e) {
|
||||||
|
log.warning('Failed to retrieve local image $localId: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteId = widget.remoteId;
|
||||||
|
if (remoteId != null) {
|
||||||
|
final uri = getThumbnailUrlForRemoteId(remoteId);
|
||||||
|
final headers = ApiService.getRequestHeaders();
|
||||||
|
final stream = _imageCache.getFileStream(
|
||||||
|
uri,
|
||||||
|
key: uri,
|
||||||
|
withProgress: true,
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
await for (final result in stream) {
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result is FileInfo) {
|
||||||
|
stopwatch.stop();
|
||||||
|
log.info(
|
||||||
|
'Retrieved remote image $remoteId in ${stopwatch.elapsedMilliseconds.toStringAsFixed(2)} ms',
|
||||||
|
);
|
||||||
|
return ImmutableBuffer.fromFilePath(result.file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
final colorScheme = context.colorScheme;
|
||||||
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
final gradient = _gradientCache[colorScheme] ??= LinearGradient(
|
||||||
|
colors: [
|
||||||
return OctoImage.fromSet(
|
colorScheme.surfaceContainer,
|
||||||
image: provider,
|
colorScheme.surfaceContainer.darken(amount: .1),
|
||||||
octoSet: OctoSet(
|
],
|
||||||
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash, fit: fit),
|
begin: Alignment.topCenter,
|
||||||
errorBuilder: _blurHashErrorBuilder(thumbHash, provider: provider, fit: fit, asset: asset),
|
end: Alignment.bottomCenter,
|
||||||
),
|
|
||||||
fadeOutDuration: const Duration(milliseconds: 100),
|
|
||||||
fadeInDuration: Duration.zero,
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
fit: fit,
|
|
||||||
placeholderFadeInDuration: Duration.zero,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return _ThumbhashLeaf(
|
||||||
|
image: _image,
|
||||||
|
fit: widget.fit,
|
||||||
|
placeholderGradient: gradient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_image?.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
|
class _ThumbhashLeaf extends LeafRenderObjectWidget {
|
||||||
return (context) => Thumbhash(blurhash: thumbHash, fit: fit ?? BoxFit.cover);
|
final ui.Image? image;
|
||||||
|
final BoxFit fit;
|
||||||
|
final Gradient placeholderGradient;
|
||||||
|
|
||||||
|
const _ThumbhashLeaf({
|
||||||
|
required this.image,
|
||||||
|
required this.fit,
|
||||||
|
required this.placeholderGradient,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return _ThumbhashRenderBox(
|
||||||
|
image: image,
|
||||||
|
fit: fit,
|
||||||
|
placeholderGradient: placeholderGradient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
_ThumbhashRenderBox renderObject,
|
||||||
|
) {
|
||||||
|
renderObject.fit = fit;
|
||||||
|
renderObject.image = image;
|
||||||
|
renderObject.placeholderGradient = placeholderGradient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) =>
|
class _ThumbhashRenderBox extends RenderBox {
|
||||||
(context, e, s) {
|
ui.Image? _image;
|
||||||
Logger("ImThumbnail").warning("Error loading thumbnail for ${asset?.name}", e, s);
|
BoxFit _fit;
|
||||||
provider?.evict();
|
Gradient _placeholderGradient;
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
@override
|
||||||
children: [
|
bool isRepaintBoundary = true;
|
||||||
_blurHashPlaceholderBuilder(blurhash, fit: fit)(context),
|
|
||||||
const Opacity(opacity: 0.75, child: Icon(Icons.error_outline_rounded)),
|
_ThumbhashRenderBox({
|
||||||
],
|
required ui.Image? image,
|
||||||
);
|
required BoxFit fit,
|
||||||
};
|
required Gradient placeholderGradient,
|
||||||
|
}) : _image = image,
|
||||||
|
_fit = fit,
|
||||||
|
_placeholderGradient = placeholderGradient;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
final image = _image;
|
||||||
|
final rect = offset & size;
|
||||||
|
if (image == null) {
|
||||||
|
final paint = Paint();
|
||||||
|
paint.shader = _placeholderGradient.createShader(rect);
|
||||||
|
context.canvas.drawRect(rect, paint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
paintImage(
|
||||||
|
canvas: context.canvas,
|
||||||
|
rect: rect,
|
||||||
|
image: image,
|
||||||
|
fit: _fit,
|
||||||
|
filterQuality: FilterQuality.low,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
size = constraints.biggest;
|
||||||
|
}
|
||||||
|
|
||||||
|
set image(ui.Image? value) {
|
||||||
|
if (_image != value) {
|
||||||
|
_image = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set fit(BoxFit value) {
|
||||||
|
if (_fit == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fit = value;
|
||||||
|
if (_image != null) {
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set placeholderGradient(Gradient value) {
|
||||||
|
if (_placeholderGradient == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_placeholderGradient = value;
|
||||||
|
if (_image == null) {
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final BaseAsset asset;
|
final BaseAsset? asset;
|
||||||
final Size size;
|
final Size size;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
@ -28,6 +28,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = this.asset;
|
||||||
final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||||
|
|
||||||
final assetContainerColor = context.isDarkTheme
|
final assetContainerColor = context.isDarkTheme
|
||||||
@ -50,7 +51,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const BoxDecoration();
|
: const BoxDecoration();
|
||||||
|
|
||||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
final hasStack = asset is RemoteAsset && asset.stackId != null;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -66,8 +67,8 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: '${asset.heroTag}_$heroIndex',
|
tag: '${asset?.heroTag ?? ''}_$heroIndex',
|
||||||
child: Thumbnail(asset: asset, fit: fit, size: size),
|
child: Thumbnail.fromBaseAsset(asset: asset, fit: fit, size: size),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasStack)
|
if (hasStack)
|
||||||
@ -78,7 +79,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
|
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.isVideo)
|
if (asset != null && asset.isVideo)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -86,7 +87,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
child: _VideoIndicator(asset.duration),
|
child: _VideoIndicator(asset.duration),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showStorageIndicator)
|
if (showStorageIndicator && asset != null)
|
||||||
switch (asset.storage) {
|
switch (asset.storage) {
|
||||||
AssetState.local => const Align(
|
AssetState.local => const Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
@ -110,7 +111,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
if (asset.isFavorite)
|
if (asset != null && asset.isFavorite)
|
||||||
const Align(
|
const Align(
|
||||||
alignment: Alignment.bottomLeft,
|
alignment: Alignment.bottomLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -7,7 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.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/images/full_image.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/full_image.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/widgets/common/thumbhash.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
|
||||||
class DriftMemoryCard extends StatelessWidget {
|
class DriftMemoryCard extends StatelessWidget {
|
||||||
final RemoteAsset asset;
|
final RemoteAsset asset;
|
||||||
@ -91,7 +91,7 @@ class _BlurredBackdrop extends HookWidget {
|
|||||||
final blurhash = asset.thumbHash;
|
final blurhash = asset.thumbHash;
|
||||||
if (blurhash != null) {
|
if (blurhash != null) {
|
||||||
// Use a nice cheap blur hash image decoration
|
// Use a nice cheap blur hash image decoration
|
||||||
return Thumbhash(blurhash: blurhash, fit: BoxFit.cover);
|
return Thumbnail(blurhash: blurhash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to using a more expensive image filtered
|
// Fall back to using a more expensive image filtered
|
||||||
|
@ -57,12 +57,15 @@ class DriftMemoryCard extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ColorFiltered(
|
ColorFiltered(
|
||||||
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
|
colorFilter: ColorFilter.mode(
|
||||||
child: SizedBox(
|
Colors.black.withValues(alpha: 0.2),
|
||||||
width: 205,
|
BlendMode.darken,
|
||||||
height: 200,
|
|
||||||
child: Thumbnail(remoteId: memory.assets[0].id, fit: BoxFit.cover),
|
|
||||||
),
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 205,
|
||||||
|
height: 200,
|
||||||
|
child: Thumbnail.fromBaseAsset(asset: memory.assets[0]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 16,
|
bottom: 16,
|
||||||
|
@ -10,8 +10,6 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.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/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@ -108,42 +106,41 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
if (isScrubbing) {
|
|
||||||
return _buildPlaceholder(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timelineService.hasRange(assetIndex, assetCount)) {
|
if (timelineService.hasRange(assetIndex, assetCount)) {
|
||||||
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService);
|
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FutureBuilder<List<BaseAsset>>(
|
try {
|
||||||
future: timelineService.loadAssets(assetIndex, assetCount),
|
final assets = timelineService.getAssets(assetIndex, assetCount);
|
||||||
builder: (context, snapshot) {
|
return FutureBuilder<List<BaseAsset>>(
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
future: null,
|
||||||
return _buildPlaceholder(context);
|
initialData: assets,
|
||||||
}
|
builder: (context, snapshot) {
|
||||||
return _buildAssetRow(context, snapshot.requireData, timelineService);
|
return _buildAssetRow(context, snapshot.data, timelineService);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return FutureBuilder<List<BaseAsset>>(
|
||||||
|
future: timelineService.loadAssets(assetIndex, assetCount),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return _buildAssetRow(context, snapshot.data, timelineService);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholder(BuildContext context) {
|
Widget _buildAssetRow(BuildContext context, List<BaseAsset>? assets, TimelineService timelineService) {
|
||||||
return SegmentBuilder.buildPlaceholder(context, assetCount, size: Size.square(tileHeight), spacing: spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets, TimelineService timelineService) {
|
|
||||||
return FixedTimelineRow(
|
return FixedTimelineRow(
|
||||||
dimension: tileHeight,
|
dimension: tileHeight,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < assets.length; i++)
|
for (int i = 0; i < assetCount; i++)
|
||||||
_AssetTileWidget(
|
_AssetTileWidget(
|
||||||
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
|
key: ValueKey(i.hashCode ^ (assetIndex + i).hashCode ^ timelineService.hashCode),
|
||||||
asset: assets[i],
|
asset: assets == null ? null : assets[i],
|
||||||
assetIndex: assetIndex + i,
|
assetIndex: assetIndex + i,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -152,7 +149,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AssetTileWidget extends ConsumerWidget {
|
class _AssetTileWidget extends ConsumerWidget {
|
||||||
final BaseAsset asset;
|
final BaseAsset? asset;
|
||||||
final int assetIndex;
|
final int assetIndex;
|
||||||
|
|
||||||
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
|
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
|
||||||
@ -201,17 +198,18 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
|
|
||||||
final lockSelection = _getLockSelectionStatus(ref);
|
final lockSelection = _getLockSelectionStatus(ref);
|
||||||
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
|
||||||
|
final asset = this.asset;
|
||||||
|
|
||||||
return RepaintBoundary(
|
return GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () => lockSelection || asset == null ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset),
|
||||||
onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset),
|
onLongPress: () => lockSelection || asset == null
|
||||||
onLongPress: () => lockSelection ? null : _handleOnLongPress(ref, asset),
|
? null
|
||||||
|
: _handleOnLongPress(ref, asset),
|
||||||
child: ThumbnailTile(
|
child: ThumbnailTile(
|
||||||
asset,
|
asset,
|
||||||
lockSelection: lockSelection,
|
lockSelection: lockSelection,
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash.dart';
|
|
||||||
|
|
||||||
abstract class SegmentBuilder {
|
abstract class SegmentBuilder {
|
||||||
final List<Bucket> buckets;
|
final List<Bucket> buckets;
|
||||||
@ -29,7 +29,7 @@ abstract class SegmentBuilder {
|
|||||||
dimension: size.height,
|
dimension: size.height,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: List.filled(count, const Thumbhash()),
|
children: List.filled(count, const Thumbnail()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
||||||
|
|
||||||
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||||
|
|
||||||
|
final thumbnailApi = ThumbnailApi();
|
||||||
|
@ -40,9 +40,9 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
child: Badge(
|
child: Badge(
|
||||||
label: Container(
|
label: const DecoratedBox(
|
||||||
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)),
|
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.all(Radius.circular(widgetSize / 2))),
|
||||||
child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
|
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
|
@ -3,9 +3,9 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.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/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash.dart';
|
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
class ImmichImage extends StatelessWidget {
|
class ImmichImage extends StatelessWidget {
|
||||||
@ -14,7 +14,7 @@ class ImmichImage extends StatelessWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.placeholder = const Thumbhash(),
|
this.placeholder = const Thumbnail(),
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:ui' as ui;
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
|
||||||
|
|
||||||
|
|
||||||
class Thumbhash extends StatefulWidget {
|
|
||||||
final String? blurhash;
|
|
||||||
final BoxFit fit;
|
|
||||||
|
|
||||||
const Thumbhash({
|
|
||||||
this.blurhash,
|
|
||||||
this.fit = BoxFit.cover,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<Thumbhash> createState() => _ThumbhashState();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _ThumbhashState extends State<Thumbhash> {
|
|
||||||
String? blurhash;
|
|
||||||
BoxFit? fit;
|
|
||||||
ui.Image? _image;
|
|
||||||
|
|
||||||
static final _gradientCache = <ColorScheme, Gradient>{};
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final blurhash_ = blurhash = widget.blurhash;
|
|
||||||
fit = widget.fit;
|
|
||||||
if (blurhash_ == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final image = thumbhash.thumbHashToRGBA(base64.decode(blurhash_));
|
|
||||||
_decode(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _decode(thumbhash.Image image) async {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(image.rgba);
|
|
||||||
if (!mounted) {
|
|
||||||
buffer.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final descriptor = ImageDescriptor.raw(
|
|
||||||
buffer,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
pixelFormat: PixelFormat.rgba8888,
|
|
||||||
);
|
|
||||||
if (!mounted) {
|
|
||||||
buffer.dispose();
|
|
||||||
descriptor.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final codec = await descriptor.instantiateCodec(
|
|
||||||
targetWidth: image.width,
|
|
||||||
targetHeight: image.height,
|
|
||||||
);
|
|
||||||
if (!mounted) {
|
|
||||||
buffer.dispose();
|
|
||||||
descriptor.dispose();
|
|
||||||
codec.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final frame = (await codec.getNextFrame()).image;
|
|
||||||
buffer.dispose();
|
|
||||||
descriptor.dispose();
|
|
||||||
codec.dispose();
|
|
||||||
if (!mounted) {
|
|
||||||
frame.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_image = frame;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final colorScheme = context.colorScheme;
|
|
||||||
final gradient = _gradientCache[colorScheme] ??= LinearGradient(
|
|
||||||
colors: [
|
|
||||||
colorScheme.surfaceContainer,
|
|
||||||
colorScheme.surfaceContainer.darken(amount: .1),
|
|
||||||
],
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
);
|
|
||||||
|
|
||||||
return _ThumbhashLeaf(
|
|
||||||
image: _image,
|
|
||||||
fit: fit!,
|
|
||||||
placeholderGradient: gradient,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_image?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ThumbhashLeaf extends LeafRenderObjectWidget {
|
|
||||||
final ui.Image? image;
|
|
||||||
final BoxFit fit;
|
|
||||||
final Gradient placeholderGradient;
|
|
||||||
|
|
||||||
const _ThumbhashLeaf({
|
|
||||||
required this.image,
|
|
||||||
required this.fit,
|
|
||||||
required this.placeholderGradient,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
|
||||||
return _ThumbhashRenderBox(
|
|
||||||
image: image,
|
|
||||||
fit: fit,
|
|
||||||
placeholderGradient: placeholderGradient,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(
|
|
||||||
BuildContext context,
|
|
||||||
_ThumbhashRenderBox renderObject,
|
|
||||||
) {
|
|
||||||
renderObject.fit = fit;
|
|
||||||
renderObject.image = image;
|
|
||||||
renderObject.placeholderGradient = placeholderGradient;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ThumbhashRenderBox extends RenderBox {
|
|
||||||
ui.Image? _image;
|
|
||||||
BoxFit _fit;
|
|
||||||
Gradient _placeholderGradient;
|
|
||||||
|
|
||||||
_ThumbhashRenderBox({
|
|
||||||
required ui.Image? image,
|
|
||||||
required BoxFit fit,
|
|
||||||
required Gradient placeholderGradient,
|
|
||||||
}) : _image = image,
|
|
||||||
_fit = fit,
|
|
||||||
_placeholderGradient = placeholderGradient;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(PaintingContext context, Offset offset) {
|
|
||||||
final image = _image;
|
|
||||||
final rect = offset & size;
|
|
||||||
if (image == null) {
|
|
||||||
final paint = Paint();
|
|
||||||
paint.shader = _placeholderGradient.createShader(rect);
|
|
||||||
context.canvas.drawRect(rect, paint);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
paintImage(
|
|
||||||
canvas: context.canvas,
|
|
||||||
rect: rect,
|
|
||||||
image: image,
|
|
||||||
fit: _fit,
|
|
||||||
filterQuality: FilterQuality.low,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performLayout() {
|
|
||||||
size = constraints.biggest;
|
|
||||||
}
|
|
||||||
|
|
||||||
set image(ui.Image? value) {
|
|
||||||
if (_image != value) {
|
|
||||||
_image = value;
|
|
||||||
markNeedsPaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set fit(BoxFit value) {
|
|
||||||
if (_fit != value) {
|
|
||||||
_fit = value;
|
|
||||||
markNeedsPaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set placeholderGradient(Gradient value) {
|
|
||||||
if (_placeholderGradient != value) {
|
|
||||||
_placeholderGradient = value;
|
|
||||||
markNeedsPaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
OctoPlaceholderBuilder blurHashPlaceholderBuilder(String? blurhash, {required BoxFit fit}) {
|
OctoPlaceholderBuilder blurHashPlaceholderBuilder(String? blurhash, {required BoxFit fit}) {
|
||||||
return (context) => Thumbhash(blurhash: blurhash, fit: fit);
|
return (context) => Thumbnail(blurhash: blurhash, fit: fit);
|
||||||
}
|
}
|
||||||
|
|
||||||
OctoErrorBuilder blurHashErrorBuilder(
|
OctoErrorBuilder blurHashErrorBuilder(
|
||||||
|
@ -5,8 +5,8 @@ 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_viewer.page.dart';
|
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash.dart';
|
|
||||||
|
|
||||||
class MemoryCard extends StatelessWidget {
|
class MemoryCard extends StatelessWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
@ -90,7 +90,7 @@ class _BlurredBackdrop extends HookWidget {
|
|||||||
final blurhash = asset.thumbhash;
|
final blurhash = asset.thumbhash;
|
||||||
if (blurhash != null) {
|
if (blurhash != null) {
|
||||||
// Use a nice cheap blur hash image decoration
|
// Use a nice cheap blur hash image decoration
|
||||||
return Thumbhash(blurhash: blurhash, fit: BoxFit.cover);
|
return Thumbnail(blurhash: blurhash, fit: BoxFit.cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to using a more expensive image filtered
|
// Fall back to using a more expensive image filtered
|
||||||
|
@ -7,7 +7,9 @@ build:
|
|||||||
|
|
||||||
pigeon:
|
pigeon:
|
||||||
dart run pigeon --input pigeon/native_sync_api.dart
|
dart run pigeon --input pigeon/native_sync_api.dart
|
||||||
|
dart run pigeon --input pigeon/thumbnail_api.dart
|
||||||
dart format lib/platform/native_sync_api.g.dart
|
dart format lib/platform/native_sync_api.g.dart
|
||||||
|
dart format lib/platform/thumbnail_api.g.dart
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
dart run build_runner watch --delete-conflicting-outputs
|
dart run build_runner watch --delete-conflicting-outputs
|
||||||
|
37
mobile/pigeon/thumbnail_api.dart
Normal file
37
mobile/pigeon/thumbnail_api.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:pigeon/pigeon.dart';
|
||||||
|
|
||||||
|
@ConfigurePigeon(
|
||||||
|
PigeonOptions(
|
||||||
|
dartOut: 'lib/platform/thumbnail_api.g.dart',
|
||||||
|
swiftOut: 'ios/Runner/Images/Thumbnails.g.swift',
|
||||||
|
swiftOptions: SwiftOptions(includeErrorClass: false),
|
||||||
|
kotlinOut:
|
||||||
|
'android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt',
|
||||||
|
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.images'),
|
||||||
|
dartOptions: DartOptions(),
|
||||||
|
dartPackageName: 'immich_mobile',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@HostApi()
|
||||||
|
abstract class ThumbnailApi {
|
||||||
|
@async
|
||||||
|
Uint8List getThumbnail(
|
||||||
|
String assetId, {
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @async
|
||||||
|
// int requestThumbnail(
|
||||||
|
// String assetId, {
|
||||||
|
// required int width,
|
||||||
|
// required int height,
|
||||||
|
// void Function(int requestId) onDone,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
@FlutterApi()
|
||||||
|
abstract class PlatformThumbnailApi {
|
||||||
|
@async
|
||||||
|
Uint8List? getThumbnail(String assetId, int width, int height);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user