mirror of
https://github.com/immich-app/immich.git
synced 2026-02-02 10:14:50 -08:00
Compare commits
27 Commits
v2.5.2
...
fix/mobile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95981e3b76 | ||
|
|
0bfe018b52 | ||
|
|
0dd34f507f | ||
|
|
c2d32b95bc | ||
|
|
3159986adb | ||
|
|
79a8031b9e | ||
|
|
8a600e0811 | ||
|
|
cc63580c1d | ||
|
|
a2f301caab | ||
|
|
fbacc0d0c0 | ||
|
|
69a50e9f7e | ||
|
|
2e911ec1ca | ||
|
|
4ed94a34f0 | ||
|
|
95c5e736b2 | ||
|
|
38dccb0f76 | ||
|
|
22b89d241f | ||
|
|
1436e2a75f | ||
|
|
855817514c | ||
|
|
d5ad35ea52 | ||
|
|
e63213d774 | ||
|
|
0be1ffade6 | ||
|
|
1a04caee29 | ||
|
|
3ace578fc0 | ||
|
|
25c573bc7a | ||
|
|
10bb83cf75 | ||
|
|
10b53b525d | ||
|
|
8db61d341f |
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -88,7 +88,7 @@ The easiest option is to have both extensions installed during the migration:
|
||||
<details>
|
||||
<summary>Migration steps (automatic)</summary>
|
||||
1. Ensure you still have pgvecto.rs installed
|
||||
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
2. Install `pgvector` (`>= 0.7, < 0.9`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
3. [Install VectorChord][vchord-install]
|
||||
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
|
||||
5. Restart the Postgres database
|
||||
|
||||
@@ -782,6 +782,8 @@
|
||||
"client_cert_import": "Import",
|
||||
"client_cert_import_success_msg": "Client certificate is imported",
|
||||
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
|
||||
"client_cert_password_message": "Enter the password for this certificate",
|
||||
"client_cert_password_title": "Certificate Password",
|
||||
"client_cert_remove_msg": "Client certificate is removed",
|
||||
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate import/removal is available only before login",
|
||||
"client_cert_title": "SSL client certificate [EXPERIMENTAL]",
|
||||
|
||||
@@ -17,9 +17,9 @@ config_roots = [
|
||||
node = "24.13.0"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.28.0"
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
java = "25.0.1"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
java = "21.0.2"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"flutter": "3.35.7"
|
||||
}
|
||||
5
mobile/.gitignore
vendored
5
mobile/.gitignore
vendored
@@ -55,8 +55,5 @@ default.isar
|
||||
default.isar.lock
|
||||
libisar.so
|
||||
|
||||
# FVM Version
|
||||
.fvm/
|
||||
|
||||
# Translation file
|
||||
lib/generated/
|
||||
lib/generated/
|
||||
|
||||
4
mobile/.vscode/settings.json
vendored
4
mobile/.vscode/settings.json
vendored
@@ -2,7 +2,9 @@
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.7",
|
||||
"dart.lineLength": 120,
|
||||
"[dart]": {
|
||||
"editor.rulers": [120]
|
||||
"editor.rulers": [
|
||||
120
|
||||
]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
|
||||
@@ -4,10 +4,12 @@ The Immich mobile app is a Flutter-based solution leveraging the Isar Database f
|
||||
|
||||
## Setup
|
||||
|
||||
1. Setup Flutter toolchain using FVM.
|
||||
2. Run `flutter pub get` to install the dependencies.
|
||||
3. Run `make translation` to generate the translation file.
|
||||
4. Run `fvm flutter run` to start the app.
|
||||
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
|
||||
2. Change to the immich directory and trust the mise config with `mise trust`.
|
||||
3. Install tools with mise: `mise install`.
|
||||
4. Run `flutter pub get` to install the dependencies.
|
||||
5. Run `make translation` to generate the translation file.
|
||||
6. Run `flutter run` to start the app.
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -29,7 +31,7 @@ dcm analyze lib
|
||||
```
|
||||
|
||||
[DCM](https://dcm.dev/) is a vendor tool that needs to be downloaded manually to run locally.
|
||||
Immich was provided an open source license.
|
||||
Immich was provided an open source license.
|
||||
To use it, it is important that you do not have an active free tier license (can be verified with `dcm license`).
|
||||
If you have write-access to the Immich repository directly, running dcm in your clone should just work.
|
||||
If you are working on a clone of a fork, you need to connect to the main Immich repository as remote first:
|
||||
|
||||
@@ -131,6 +131,7 @@ dependencies {
|
||||
implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
implementation "androidx.compose.material3:material3:1.2.1"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
}
|
||||
|
||||
// This is uncommented in F-Droid build script
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
|
||||
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
||||
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
|
||||
android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false">
|
||||
android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<profileable android:shell="true" />
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import app.alextran.immich.core.SSLConfig
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLEngine
|
||||
import javax.net.ssl.SSLSession
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedTrustManager
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `HttpSSLOptions`
|
||||
*/
|
||||
class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private var methodChannel: MethodChannel? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
}
|
||||
|
||||
private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
|
||||
methodChannel = MethodChannel(messenger, "immich/httpSSLOptions")
|
||||
methodChannel?.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onDetachedFromEngine()
|
||||
}
|
||||
|
||||
private fun onDetachedFromEngine() {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
try {
|
||||
when (call.method) {
|
||||
"apply" -> {
|
||||
val args = call.arguments<ArrayList<*>>()!!
|
||||
val allowSelfSigned = args[0] as Boolean
|
||||
val serverHost = args[1] as? String
|
||||
val clientCertHash = (args[2] as? ByteArray)
|
||||
|
||||
var tm: Array<TrustManager>? = null
|
||||
if (allowSelfSigned) {
|
||||
tm = arrayOf(AllowSelfSignedTrustManager(serverHost))
|
||||
}
|
||||
|
||||
var km: Array<KeyManager>? = null
|
||||
if (clientCertHash != null) {
|
||||
val cert = ByteArrayInputStream(clientCertHash)
|
||||
val password = (args[3] as String).toCharArray()
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
keyStore.load(cert, password)
|
||||
val keyManagerFactory =
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore, null)
|
||||
km = keyManagerFactory.keyManagers
|
||||
}
|
||||
|
||||
// Update shared SSL config for OkHttp and other HTTP clients
|
||||
SSLConfig.apply(km, tm, allowSelfSigned, serverHost, clientCertHash?.contentHashCode() ?: 0)
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(km, tm, null)
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(AllowSelfSignedHostnameVerifier(args[1] as? String))
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
result.error("error", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
class AllowSelfSignedTrustManager(private val serverHost: String?) : X509ExtendedTrustManager() {
|
||||
private val defaultTrustManager: X509ExtendedTrustManager = getDefaultTrustManager()
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) =
|
||||
defaultTrustManager.checkClientTrusted(chain, authType)
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, socket: Socket?
|
||||
) = defaultTrustManager.checkClientTrusted(chain, authType, socket)
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?
|
||||
) = defaultTrustManager.checkClientTrusted(chain, authType, engine)
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
if (serverHost == null) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, socket: Socket?
|
||||
) {
|
||||
if (serverHost == null) return
|
||||
val socketAddress = socket?.remoteSocketAddress
|
||||
if (socketAddress is InetSocketAddress && socketAddress.hostName == serverHost) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType, socket)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?, authType: String?, engine: SSLEngine?
|
||||
) {
|
||||
if (serverHost == null || engine?.peerHost == serverHost) return
|
||||
defaultTrustManager.checkServerTrusted(chain, authType, engine)
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = defaultTrustManager.acceptedIssuers
|
||||
|
||||
private fun getDefaultTrustManager(): X509ExtendedTrustManager {
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
return factory.trustManagers.filterIsInstance<X509ExtendedTrustManager>().first()
|
||||
}
|
||||
}
|
||||
|
||||
class AllowSelfSignedHostnameVerifier(private val serverHost: String?) : HostnameVerifier {
|
||||
companion object {
|
||||
private val _defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
|
||||
}
|
||||
|
||||
override fun verify(hostname: String?, session: SSLSession?): Boolean {
|
||||
if (serverHost == null || hostname == serverHost) {
|
||||
return true
|
||||
} else {
|
||||
return _defaultHostnameVerifier.verify(hostname, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
||||
import app.alextran.immich.background.BackgroundWorkerLockApi
|
||||
import app.alextran.immich.connectivity.ConnectivityApi
|
||||
import app.alextran.immich.connectivity.ConnectivityApiImpl
|
||||
import app.alextran.immich.core.HttpClientManager
|
||||
import app.alextran.immich.core.ImmichPlugin
|
||||
import app.alextran.immich.core.NetworkApiPlugin
|
||||
import app.alextran.immich.images.LocalImageApi
|
||||
import app.alextran.immich.images.LocalImagesImpl
|
||||
import app.alextran.immich.images.RemoteImageApi
|
||||
@@ -28,6 +30,9 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
companion object {
|
||||
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
||||
HttpClientManager.initialize(ctx)
|
||||
flutterEngine.plugins.add(NetworkApiPlugin())
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
val backgroundEngineLockImpl = BackgroundEngineLock(ctx)
|
||||
BackgroundWorkerLockApi.setUp(messenger, backgroundEngineLockImpl)
|
||||
@@ -45,7 +50,6 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||
|
||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
|
||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package app.alextran.immich.core
|
||||
|
||||
import android.content.Context
|
||||
import app.alextran.immich.BuildConfig
|
||||
import okhttp3.Cache
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509KeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
const val CERT_ALIAS = "client_cert"
|
||||
const val USER_AGENT = "Immich_Android_${BuildConfig.VERSION_NAME}"
|
||||
|
||||
/**
|
||||
* Manages a shared OkHttpClient with SSL configuration support.
|
||||
*/
|
||||
object HttpClientManager {
|
||||
private const val CACHE_SIZE_BYTES = 100L * 1024 * 1024 // 100MiB
|
||||
private const val KEEP_ALIVE_CONNECTIONS = 10
|
||||
private const val KEEP_ALIVE_DURATION_MINUTES = 5L
|
||||
private const val MAX_REQUESTS_PER_HOST = 64
|
||||
|
||||
private var initialized = false
|
||||
private val clientChangedListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
private lateinit var client: OkHttpClient
|
||||
|
||||
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
|
||||
val isMtls: Boolean get() = keyStore.containsAlias(CERT_ALIAS)
|
||||
|
||||
fun initialize(context: Context) {
|
||||
if (initialized) return
|
||||
synchronized(this) {
|
||||
if (initialized) return
|
||||
|
||||
val cacheDir = File(File(context.cacheDir, "okhttp"), "api")
|
||||
client = build(cacheDir)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setKeyEntry(clientData: ByteArray, password: CharArray) {
|
||||
synchronized(this) {
|
||||
val wasMtls = isMtls
|
||||
val tmpKeyStore = KeyStore.getInstance("PKCS12").apply {
|
||||
ByteArrayInputStream(clientData).use { stream -> load(stream, password) }
|
||||
}
|
||||
val tmpAlias = tmpKeyStore.aliases().asSequence().firstOrNull { tmpKeyStore.isKeyEntry(it) }
|
||||
?: throw IllegalArgumentException("No private key found in PKCS12")
|
||||
val key = tmpKeyStore.getKey(tmpAlias, password)
|
||||
val chain = tmpKeyStore.getCertificateChain(tmpAlias)
|
||||
|
||||
if (wasMtls) {
|
||||
keyStore.deleteEntry(CERT_ALIAS)
|
||||
}
|
||||
keyStore.setKeyEntry(CERT_ALIAS, key, null, chain)
|
||||
if (wasMtls != isMtls) {
|
||||
clientChangedListeners.forEach { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteKeyEntry() {
|
||||
synchronized(this) {
|
||||
if (!isMtls) {
|
||||
return
|
||||
}
|
||||
|
||||
keyStore.deleteEntry(CERT_ALIAS)
|
||||
clientChangedListeners.forEach { it() }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getClient(): OkHttpClient {
|
||||
return client
|
||||
}
|
||||
|
||||
fun addClientChangedListener(listener: () -> Unit) {
|
||||
synchronized(this) { clientChangedListeners.add(listener) }
|
||||
}
|
||||
|
||||
private fun build(cacheDir: File): OkHttpClient {
|
||||
val connectionPool = ConnectionPool(
|
||||
maxIdleConnections = KEEP_ALIVE_CONNECTIONS,
|
||||
keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES,
|
||||
timeUnit = TimeUnit.MINUTES
|
||||
)
|
||||
|
||||
val managerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
managerFactory.init(null as KeyStore?)
|
||||
val trustManager = managerFactory.trustManagers.filterIsInstance<X509TrustManager>().first()
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
.apply { init(arrayOf(DynamicKeyManager()), arrayOf(trustManager), null) }
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
chain.proceed(chain.request().newBuilder().header("User-Agent", USER_AGENT).build())
|
||||
}
|
||||
.connectionPool(connectionPool)
|
||||
.dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST })
|
||||
.cache(Cache(cacheDir.apply { mkdirs() }, CACHE_SIZE_BYTES))
|
||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Reads from the key store rather than taking a snapshot at initialization time
|
||||
private class DynamicKeyManager : X509KeyManager {
|
||||
override fun getClientAliases(keyType: String, issuers: Array<Principal>?): Array<String>? =
|
||||
if (isMtls) arrayOf(CERT_ALIAS) else null
|
||||
|
||||
override fun chooseClientAlias(
|
||||
keyTypes: Array<String>,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
): String? =
|
||||
if (isMtls) CERT_ALIAS else null
|
||||
|
||||
override fun getCertificateChain(alias: String): Array<X509Certificate>? =
|
||||
keyStore.getCertificateChain(alias)?.map { it as X509Certificate }?.toTypedArray()
|
||||
|
||||
override fun getPrivateKey(alias: String): PrivateKey? =
|
||||
keyStore.getKey(alias, null) as? PrivateKey
|
||||
|
||||
override fun getServerAliases(keyType: String, issuers: Array<Principal>?): Array<String>? =
|
||||
null
|
||||
|
||||
override fun chooseServerAlias(
|
||||
keyType: String,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
): String? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package app.alextran.immich.core
|
||||
|
||||
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 NetworkPigeonUtils {
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
}
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class ClientCertData (
|
||||
val data: ByteArray,
|
||||
val password: String
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ClientCertData {
|
||||
val data = pigeonVar_list[0] as ByteArray
|
||||
val password = pigeonVar_list[1] as String
|
||||
return ClientCertData(data, password)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
data,
|
||||
password,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ClientCertData) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class ClientCertPrompt (
|
||||
val title: String,
|
||||
val message: String,
|
||||
val cancel: String,
|
||||
val confirm: String
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ClientCertPrompt {
|
||||
val title = pigeonVar_list[0] as String
|
||||
val message = pigeonVar_list[1] as String
|
||||
val cancel = pigeonVar_list[2] as String
|
||||
val confirm = pigeonVar_list[3] as String
|
||||
return ClientCertPrompt(title, message, cancel, confirm)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
title,
|
||||
message,
|
||||
cancel,
|
||||
confirm,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ClientCertPrompt) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
private open class NetworkPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ClientCertData.fromList(it)
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ClientCertPrompt.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is ClientCertData -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ClientCertPrompt -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface NetworkApi {
|
||||
fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit)
|
||||
fun selectCertificate(promptText: ClientCertPrompt, callback: (Result<ClientCertData>) -> Unit)
|
||||
fun removeCertificate(callback: (Result<Unit>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by NetworkApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
NetworkPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val clientDataArg = args[0] as ClientCertData
|
||||
api.addCertificate(clientDataArg) { result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(NetworkPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(NetworkPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val promptTextArg = args[0] as ClientCertPrompt
|
||||
api.selectCertificate(promptTextArg) { result: Result<ClientCertData> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(NetworkPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(NetworkPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.removeCertificate{ result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(NetworkPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(NetworkPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package app.alextran.immich.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.OperationCanceledException
|
||||
import android.text.InputType
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
|
||||
class NetworkApiPlugin : FlutterPlugin, ActivityAware {
|
||||
private var networkApi: NetworkApiImpl? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
networkApi = NetworkApiImpl(binding.applicationContext)
|
||||
NetworkApi.setUp(binding.binaryMessenger, networkApi)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
NetworkApi.setUp(binding.binaryMessenger, null)
|
||||
networkApi = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
networkApi?.onAttachedToActivity(binding)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
networkApi?.onDetachedFromActivityForConfigChanges()
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
networkApi?.onReattachedToActivityForConfigChanges(binding)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
networkApi?.onDetachedFromActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkApiImpl(private val context: Context) : NetworkApi {
|
||||
private var activity: Activity? = null
|
||||
private var pendingCallback: ((Result<ClientCertData>) -> Unit)? = null
|
||||
private var filePicker: ActivityResultLauncher<Array<String>>? = null
|
||||
private var promptText: ClientCertPrompt? = null
|
||||
|
||||
fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
(binding.activity as? ComponentActivity)?.let { componentActivity ->
|
||||
filePicker = componentActivity.registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri -> uri?.let { handlePickedFile(it) } ?: pendingCallback?.invoke(Result.failure(OperationCanceledException())) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
|
||||
try {
|
||||
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
|
||||
callback(Result.success(Unit))
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectCertificate(promptText: ClientCertPrompt, callback: (Result<ClientCertData>) -> Unit) {
|
||||
val picker = filePicker ?: return callback(Result.failure(IllegalStateException("No activity")))
|
||||
pendingCallback = callback
|
||||
this.promptText = promptText
|
||||
picker.launch(arrayOf("application/x-pkcs12", "application/x-pem-file"))
|
||||
}
|
||||
|
||||
override fun removeCertificate(callback: (Result<Unit>) -> Unit) {
|
||||
HttpClientManager.deleteKeyEntry()
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
|
||||
private fun handlePickedFile(uri: Uri) {
|
||||
val callback = pendingCallback ?: return
|
||||
pendingCallback = null
|
||||
|
||||
try {
|
||||
val data = context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
?: throw IllegalStateException("Could not read file")
|
||||
|
||||
val activity = activity ?: throw IllegalStateException("No activity")
|
||||
promptForPassword(activity) { password ->
|
||||
promptText = null
|
||||
if (password == null) {
|
||||
callback(Result.failure(OperationCanceledException()))
|
||||
return@promptForPassword
|
||||
}
|
||||
try {
|
||||
HttpClientManager.setKeyEntry(data, password.toCharArray())
|
||||
callback(Result.success(ClientCertData(data, password)))
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptForPassword(activity: Activity, callback: (String?) -> Unit) {
|
||||
val themedContext = ContextThemeWrapper(activity, com.google.android.material.R.style.Theme_Material3_DayNight_Dialog)
|
||||
val density = activity.resources.displayMetrics.density
|
||||
val horizontalPadding = (24 * density).toInt()
|
||||
|
||||
val textInputLayout = TextInputLayout(themedContext).apply {
|
||||
hint = "Password"
|
||||
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
|
||||
setMargins(horizontalPadding, 0, horizontalPadding, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val editText = TextInputEditText(textInputLayout.context).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
}
|
||||
textInputLayout.addView(editText)
|
||||
|
||||
val container = FrameLayout(themedContext).apply { addView(textInputLayout) }
|
||||
|
||||
val text = promptText!!
|
||||
MaterialAlertDialogBuilder(themedContext)
|
||||
.setTitle(text.title)
|
||||
.setMessage(text.message)
|
||||
.setView(container)
|
||||
.setPositiveButton(text.confirm) { _, _ -> callback(editText.text.toString()) }
|
||||
.setNegativeButton(text.cancel) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
callback(null)
|
||||
}
|
||||
.setOnCancelListener { callback(null) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package app.alextran.immich.core
|
||||
|
||||
import java.security.KeyStore
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* Shared SSL configuration for OkHttp and HttpsURLConnection.
|
||||
* Stores the SSLSocketFactory and X509TrustManager configured by HttpSSLOptionsPlugin.
|
||||
*/
|
||||
object SSLConfig {
|
||||
var sslSocketFactory: SSLSocketFactory? = null
|
||||
private set
|
||||
|
||||
var trustManager: X509TrustManager? = null
|
||||
private set
|
||||
|
||||
var requiresCustomSSL: Boolean = false
|
||||
private set
|
||||
|
||||
private val listeners = mutableListOf<() -> Unit>()
|
||||
private var configHash: Int = 0
|
||||
|
||||
fun addListener(listener: () -> Unit) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun apply(
|
||||
keyManagers: Array<KeyManager>?,
|
||||
trustManagers: Array<TrustManager>?,
|
||||
allowSelfSigned: Boolean,
|
||||
serverHost: String?,
|
||||
clientCertHash: Int
|
||||
) {
|
||||
synchronized(this) {
|
||||
val newHash = computeHash(allowSelfSigned, serverHost, clientCertHash)
|
||||
val newRequiresCustomSSL = allowSelfSigned || keyManagers != null
|
||||
if (newHash == configHash && sslSocketFactory != null && requiresCustomSSL == newRequiresCustomSSL) {
|
||||
return // Config unchanged, skip
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(keyManagers, trustManagers, null)
|
||||
sslSocketFactory = sslContext.socketFactory
|
||||
trustManager = trustManagers?.filterIsInstance<X509TrustManager>()?.firstOrNull()
|
||||
?: getDefaultTrustManager()
|
||||
requiresCustomSSL = newRequiresCustomSSL
|
||||
configHash = newHash
|
||||
notifyListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeHash(allowSelfSigned: Boolean, serverHost: String?, clientCertHash: Int): Int {
|
||||
var result = allowSelfSigned.hashCode()
|
||||
result = 31 * result + (serverHost?.hashCode() ?: 0)
|
||||
result = 31 * result + clientCertHash
|
||||
return result
|
||||
}
|
||||
|
||||
private fun notifyListeners() {
|
||||
listeners.forEach { it() }
|
||||
}
|
||||
|
||||
private fun getDefaultTrustManager(): X509TrustManager {
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
return factory.trustManagers.filterIsInstance<X509TrustManager>().first()
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,15 @@ package app.alextran.immich.images
|
||||
import android.content.Context
|
||||
import android.os.CancellationSignal
|
||||
import android.os.OperationCanceledException
|
||||
import app.alextran.immich.BuildConfig
|
||||
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
||||
import app.alextran.immich.NativeBuffer
|
||||
import app.alextran.immich.NativeByteBuffer
|
||||
import app.alextran.immich.core.SSLConfig
|
||||
import app.alextran.immich.core.HttpClientManager
|
||||
import app.alextran.immich.core.USER_AGENT
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@@ -32,15 +30,8 @@ import java.nio.file.SimpleFileVisitor
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
|
||||
private const val USER_AGENT = "Immich_Android_${BuildConfig.VERSION_NAME}"
|
||||
private const val MAX_REQUESTS_PER_HOST = 64
|
||||
private const val KEEP_ALIVE_CONNECTIONS = 10
|
||||
private const val KEEP_ALIVE_DURATION_MINUTES = 5L
|
||||
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
|
||||
|
||||
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||
@@ -121,7 +112,7 @@ private object ImageFetcherManager {
|
||||
appContext = context.applicationContext
|
||||
cacheDir = context.cacheDir
|
||||
fetcher = build()
|
||||
SSLConfig.addListener(::invalidate)
|
||||
HttpClientManager.addClientChangedListener(::invalidate)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
@@ -143,18 +134,14 @@ private object ImageFetcherManager {
|
||||
private fun invalidate() {
|
||||
synchronized(this) {
|
||||
val oldFetcher = fetcher
|
||||
if (oldFetcher is OkHttpImageFetcher && SSLConfig.requiresCustomSSL) {
|
||||
fetcher = oldFetcher.reconfigure(SSLConfig.sslSocketFactory, SSLConfig.trustManager)
|
||||
return
|
||||
}
|
||||
fetcher = build()
|
||||
oldFetcher.drain()
|
||||
}
|
||||
}
|
||||
|
||||
private fun build(): ImageFetcher {
|
||||
return if (SSLConfig.requiresCustomSSL) {
|
||||
OkHttpImageFetcher.create(cacheDir, SSLConfig.sslSocketFactory, SSLConfig.trustManager)
|
||||
return if (HttpClientManager.isMtls) {
|
||||
OkHttpImageFetcher.create(cacheDir)
|
||||
} else {
|
||||
CronetImageFetcher(appContext, cacheDir)
|
||||
}
|
||||
@@ -380,51 +367,17 @@ private class OkHttpImageFetcher private constructor(
|
||||
private var draining = false
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
cacheDir: File,
|
||||
sslSocketFactory: SSLSocketFactory?,
|
||||
trustManager: X509TrustManager?,
|
||||
): OkHttpImageFetcher {
|
||||
fun create(cacheDir: File): OkHttpImageFetcher {
|
||||
val dir = File(cacheDir, "okhttp")
|
||||
val connectionPool = ConnectionPool(
|
||||
maxIdleConnections = KEEP_ALIVE_CONNECTIONS,
|
||||
keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES,
|
||||
timeUnit = TimeUnit.MINUTES
|
||||
)
|
||||
|
||||
val builder = OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
chain.proceed(
|
||||
chain.request().newBuilder()
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST })
|
||||
.connectionPool(connectionPool)
|
||||
val client = HttpClientManager.getClient().newBuilder()
|
||||
.cache(Cache(File(dir, "thumbnails"), CACHE_SIZE_BYTES))
|
||||
.build()
|
||||
|
||||
if (sslSocketFactory != null && trustManager != null) {
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager)
|
||||
}
|
||||
|
||||
return OkHttpImageFetcher(builder.build())
|
||||
return OkHttpImageFetcher(client)
|
||||
}
|
||||
}
|
||||
|
||||
fun reconfigure(
|
||||
sslSocketFactory: SSLSocketFactory?,
|
||||
trustManager: X509TrustManager?,
|
||||
): OkHttpImageFetcher {
|
||||
val builder = client.newBuilder()
|
||||
if (sslSocketFactory != null && trustManager != null) {
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager)
|
||||
}
|
||||
// Evict idle connections using old SSL config
|
||||
client.connectionPool.evictAll()
|
||||
return OkHttpImageFetcher(builder.build())
|
||||
}
|
||||
|
||||
private fun onComplete() {
|
||||
val shouldClose = synchronized(stateLock) {
|
||||
activeCount--
|
||||
@@ -512,7 +465,6 @@ private class OkHttpImageFetcher private constructor(
|
||||
draining = true
|
||||
activeCount == 0
|
||||
}
|
||||
client.connectionPool.evictAll()
|
||||
if (shouldClose) {
|
||||
client.cache?.close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config cleartextTrafficPermitted="true">
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -11,40 +11,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
@@ -93,9 +59,6 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- share_handler_ios (0.0.14):
|
||||
- Flutter
|
||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||
@@ -131,7 +94,6 @@ PODS:
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
@@ -143,7 +105,6 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
@@ -176,13 +137,9 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- MapLibre
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
background_downloader:
|
||||
@@ -195,8 +152,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_local_notifications:
|
||||
@@ -262,9 +217,6 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
@@ -288,7 +240,6 @@ SPEC CHECKSUMS:
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
@@ -296,7 +247,6 @@ SPEC CHECKSUMS:
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F22F1197D8006016CB /* RemoteImages.g.swift */; };
|
||||
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F52F11980E006016CB /* LocalImagesImpl.swift */; };
|
||||
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */; };
|
||||
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */; };
|
||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
||||
@@ -124,6 +125,7 @@
|
||||
FE5499F22F1197D8006016CB /* RemoteImages.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImages.g.swift; sourceTree = "<group>"; };
|
||||
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImagesImpl.swift; sourceTree = "<group>"; };
|
||||
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = "<group>"; };
|
||||
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
|
||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -325,6 +327,7 @@
|
||||
FED3B1952E253E9B0030FD97 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */,
|
||||
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */,
|
||||
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */,
|
||||
FE5499F12F1197D8006016CB /* LocalImages.g.swift */,
|
||||
@@ -609,6 +612,7 @@
|
||||
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
|
||||
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
|
||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
|
||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
||||
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
|
||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||
|
||||
@@ -15,12 +15,12 @@ import UIKit
|
||||
) -> Bool {
|
||||
// Required for flutter_local_notification
|
||||
if #available(iOS 10.0, *) {
|
||||
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
|
||||
}
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||
AppDelegate.registerPlugins(with: controller.engine)
|
||||
AppDelegate.registerPlugins(with: controller.engine, controller: controller)
|
||||
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
|
||||
|
||||
BackgroundServicePlugin.registerBackgroundProcessing()
|
||||
@@ -51,12 +51,13 @@ import UIKit
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
public static func registerPlugins(with engine: FlutterEngine) {
|
||||
public static func registerPlugins(with engine: FlutterEngine, controller: FlutterViewController?) {
|
||||
NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
||||
LocalImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: LocalImageApiImpl())
|
||||
RemoteImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: RemoteImageApiImpl())
|
||||
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl())
|
||||
ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl())
|
||||
NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller))
|
||||
}
|
||||
|
||||
public static func cancelPlugins(with engine: FlutterEngine) {
|
||||
|
||||
@@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
// Register plugins in the new engine
|
||||
GeneratedPluginRegistrant.register(with: engine)
|
||||
// Register custom plugins
|
||||
AppDelegate.registerPlugins(with: engine)
|
||||
AppDelegate.registerPlugins(with: engine, controller: nil)
|
||||
flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger)
|
||||
BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self)
|
||||
|
||||
|
||||
284
mobile/ios/Runner/Core/Network.g.swift
Normal file
284
mobile/ios/Runner/Core/Network.g.swift
Normal file
@@ -0,0 +1,284 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import Flutter
|
||||
#elseif os(macOS)
|
||||
import FlutterMacOS
|
||||
#else
|
||||
#error("Unsupported platform.")
|
||||
#endif
|
||||
|
||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||
return [result]
|
||||
}
|
||||
|
||||
private func wrapError(_ error: Any) -> [Any?] {
|
||||
if let pigeonError = error as? PigeonError {
|
||||
return [
|
||||
pigeonError.code,
|
||||
pigeonError.message,
|
||||
pigeonError.details,
|
||||
]
|
||||
}
|
||||
if let flutterError = error as? FlutterError {
|
||||
return [
|
||||
flutterError.code,
|
||||
flutterError.message,
|
||||
flutterError.details,
|
||||
]
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
private func isNullish(_ value: Any?) -> Bool {
|
||||
return value is NSNull || value == nil
|
||||
}
|
||||
|
||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
if value is NSNull { return nil }
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
let cleanLhs = nilOrValue(lhs) as Any?
|
||||
let cleanRhs = nilOrValue(rhs) as Any?
|
||||
switch (cleanLhs, cleanRhs) {
|
||||
case (nil, nil):
|
||||
return true
|
||||
|
||||
case (nil, _), (_, nil):
|
||||
return false
|
||||
|
||||
case is (Void, Void):
|
||||
return true
|
||||
|
||||
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
|
||||
return cleanLhsHashable == cleanRhsHashable
|
||||
|
||||
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
|
||||
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
|
||||
for (index, element) in cleanLhsArray.enumerated() {
|
||||
if !deepEqualsNetwork(element, cleanRhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
|
||||
for (key, cleanLhsValue) in cleanLhsDictionary {
|
||||
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
|
||||
if !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deepHashNetwork(value: Any?, hasher: inout Hasher) {
|
||||
if let valueList = value as? [AnyHashable] {
|
||||
for item in valueList { deepHashNetwork(value: item, hasher: &hasher) }
|
||||
return
|
||||
}
|
||||
|
||||
if let valueDict = value as? [AnyHashable: AnyHashable] {
|
||||
for key in valueDict.keys {
|
||||
hasher.combine(key)
|
||||
deepHashNetwork(value: valueDict[key]!, hasher: &hasher)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let hashableValue = value as? AnyHashable {
|
||||
hasher.combine(hashableValue.hashValue)
|
||||
}
|
||||
|
||||
return hasher.combine(String(describing: value))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct ClientCertData: Hashable {
|
||||
var data: FlutterStandardTypedData
|
||||
var password: String
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertData? {
|
||||
let data = pigeonVar_list[0] as! FlutterStandardTypedData
|
||||
let password = pigeonVar_list[1] as! String
|
||||
|
||||
return ClientCertData(
|
||||
data: data,
|
||||
password: password
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
data,
|
||||
password,
|
||||
]
|
||||
}
|
||||
static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool {
|
||||
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashNetwork(value: toList(), hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct ClientCertPrompt: Hashable {
|
||||
var title: String
|
||||
var message: String
|
||||
var cancel: String
|
||||
var confirm: String
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertPrompt? {
|
||||
let title = pigeonVar_list[0] as! String
|
||||
let message = pigeonVar_list[1] as! String
|
||||
let cancel = pigeonVar_list[2] as! String
|
||||
let confirm = pigeonVar_list[3] as! String
|
||||
|
||||
return ClientCertPrompt(
|
||||
title: title,
|
||||
message: message,
|
||||
cancel: cancel,
|
||||
confirm: confirm
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
title,
|
||||
message,
|
||||
cancel,
|
||||
confirm,
|
||||
]
|
||||
}
|
||||
static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool {
|
||||
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashNetwork(value: toList(), hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkPigeonCodecReader: FlutterStandardReader {
|
||||
override func readValue(ofType type: UInt8) -> Any? {
|
||||
switch type {
|
||||
case 129:
|
||||
return ClientCertData.fromList(self.readValue() as! [Any?])
|
||||
case 130:
|
||||
return ClientCertPrompt.fromList(self.readValue() as! [Any?])
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkPigeonCodecWriter: FlutterStandardWriter {
|
||||
override func writeValue(_ value: Any) {
|
||||
if let value = value as? ClientCertData {
|
||||
super.writeByte(129)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? ClientCertPrompt {
|
||||
super.writeByte(130)
|
||||
super.writeValue(value.toList())
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return NetworkPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return NetworkPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = NetworkPigeonCodec(readerWriter: NetworkPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol NetworkApi {
|
||||
func addCertificate(clientData: ClientCertData, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result<ClientCertData, Error>) -> Void)
|
||||
func removeCertificate(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class NetworkApiSetup {
|
||||
static var codec: FlutterStandardMessageCodec { NetworkPigeonCodec.shared }
|
||||
/// Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
let addCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
addCertificateChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let clientDataArg = args[0] as! ClientCertData
|
||||
api.addCertificate(clientData: clientDataArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addCertificateChannel.setMessageHandler(nil)
|
||||
}
|
||||
let selectCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
selectCertificateChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let promptTextArg = args[0] as! ClientCertPrompt
|
||||
api.selectCertificate(promptText: promptTextArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectCertificateChannel.setMessageHandler(nil)
|
||||
}
|
||||
let removeCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
removeCertificateChannel.setMessageHandler { _, reply in
|
||||
api.removeCertificate { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
removeCertificateChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
157
mobile/ios/Runner/Core/NetworkApiImpl.swift
Normal file
157
mobile/ios/Runner/Core/NetworkApiImpl.swift
Normal file
@@ -0,0 +1,157 @@
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum ImportError: Error {
|
||||
case noFile
|
||||
case noViewController
|
||||
case keychainError(OSStatus)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
class NetworkApiImpl: NetworkApi {
|
||||
weak var viewController: UIViewController?
|
||||
private var activeImporter: CertImporter?
|
||||
|
||||
init(viewController: UIViewController?) {
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result<ClientCertData, any Error>) -> Void) {
|
||||
let importer = CertImporter(promptText: promptText, completion: { [weak self] result in
|
||||
self?.activeImporter = nil
|
||||
completion(result.map { ClientCertData(data: FlutterStandardTypedData(bytes: $0.0), password: $0.1) })
|
||||
}, viewController: viewController)
|
||||
activeImporter = importer
|
||||
importer.load()
|
||||
}
|
||||
|
||||
func removeCertificate(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
let status = clearCerts()
|
||||
if status == errSecSuccess || status == errSecItemNotFound {
|
||||
return completion(.success(()))
|
||||
}
|
||||
completion(.failure(ImportError.keychainError(status)))
|
||||
}
|
||||
|
||||
func addCertificate(clientData: ClientCertData, completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
let status = importCert(clientData: clientData.data.data, password: clientData.password)
|
||||
if status == errSecSuccess {
|
||||
return completion(.success(()))
|
||||
}
|
||||
completion(.failure(ImportError.keychainError(status)))
|
||||
}
|
||||
}
|
||||
|
||||
private class CertImporter: NSObject, UIDocumentPickerDelegate {
|
||||
private let promptText: ClientCertPrompt
|
||||
private var completion: ((Result<(Data, String), Error>) -> Void)
|
||||
private weak var viewController: UIViewController?
|
||||
|
||||
init(promptText: ClientCertPrompt, completion: (@escaping (Result<(Data, String), Error>) -> Void), viewController: UIViewController?) {
|
||||
self.promptText = promptText
|
||||
self.completion = completion
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func load() {
|
||||
guard let vc = viewController else { return completion(.failure(ImportError.noViewController)) }
|
||||
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [
|
||||
UTType(filenameExtension: "p12")!,
|
||||
UTType(filenameExtension: "pfx")!,
|
||||
])
|
||||
picker.delegate = self
|
||||
picker.allowsMultipleSelection = false
|
||||
vc.present(picker, animated: true)
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else {
|
||||
return completion(.failure(ImportError.noFile))
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let data = try readSecurityScoped(url: url)
|
||||
guard let password = await promptForPassword() else {
|
||||
return completion(.failure(ImportError.cancelled))
|
||||
}
|
||||
let status = importCert(clientData: data, password: password)
|
||||
if status != errSecSuccess {
|
||||
return completion(.failure(ImportError.keychainError(status)))
|
||||
}
|
||||
|
||||
await URLSessionManager.shared.session.flush()
|
||||
self.completion(.success((data, password)))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
completion(.failure(ImportError.cancelled))
|
||||
}
|
||||
|
||||
private func promptForPassword() async -> String? {
|
||||
guard let vc = viewController else { return nil }
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
let alert = UIAlertController(
|
||||
title: promptText.title,
|
||||
message: promptText.message,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addTextField { $0.isSecureTextEntry = true }
|
||||
|
||||
alert.addAction(UIAlertAction(title: promptText.cancel, style: .cancel) { _ in
|
||||
continuation.resume(returning: nil)
|
||||
})
|
||||
|
||||
alert.addAction(UIAlertAction(title: promptText.confirm, style: .default) { _ in
|
||||
continuation.resume(returning: alert.textFields?.first?.text ?? "")
|
||||
})
|
||||
|
||||
vc.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func readSecurityScoped(url: URL) throws -> Data {
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
throw ImportError.noFile
|
||||
}
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
}
|
||||
|
||||
private func importCert(clientData: Data, password: String) -> OSStatus {
|
||||
let options = [kSecImportExportPassphrase: password] as CFDictionary
|
||||
var items: CFArray?
|
||||
let status = SecPKCS12Import(clientData as CFData, options, &items)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let array = items as? [[String: Any]],
|
||||
let first = array.first,
|
||||
let identity = first[kSecImportItemIdentity as String] else {
|
||||
return status
|
||||
}
|
||||
|
||||
clearCerts()
|
||||
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassIdentity,
|
||||
kSecValueRef as String: identity,
|
||||
kSecAttrLabel as String: CLIENT_CERT_LABEL,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
]
|
||||
return SecItemAdd(addQuery as CFDictionary, nil)
|
||||
}
|
||||
|
||||
@discardableResult private func clearCerts() -> OSStatus {
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassIdentity,
|
||||
kSecAttrLabel as String: CLIENT_CERT_LABEL,
|
||||
]
|
||||
return SecItemDelete(deleteQuery as CFDictionary)
|
||||
}
|
||||
87
mobile/ios/Runner/Core/URLSessionManager.swift
Normal file
87
mobile/ios/Runner/Core/URLSessionManager.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
|
||||
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
|
||||
|
||||
/// Manages a shared URLSession with SSL configuration support.
|
||||
class URLSessionManager: NSObject {
|
||||
static let shared = URLSessionManager()
|
||||
|
||||
let session: URLSession
|
||||
private let configuration = {
|
||||
let config = URLSessionConfiguration.default
|
||||
|
||||
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
|
||||
.first!
|
||||
.appendingPathComponent("api", isDirectory: true)
|
||||
try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
|
||||
|
||||
config.urlCache = URLCache(
|
||||
memoryCapacity: 0,
|
||||
diskCapacity: 1024 * 1024 * 1024,
|
||||
directory: cacheDir
|
||||
)
|
||||
|
||||
config.httpMaximumConnectionsPerHost = 64
|
||||
config.timeoutIntervalForRequest = 60
|
||||
config.timeoutIntervalForResource = 300
|
||||
|
||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||
config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"]
|
||||
|
||||
return config
|
||||
}()
|
||||
|
||||
private override init() {
|
||||
session = URLSession(configuration: configuration, delegate: URLSessionManagerDelegate(), delegateQueue: nil)
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate {
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
handleChallenge(challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
handleChallenge(challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func handleChallenge(
|
||||
_ challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
switch challenge.protectionSpace.authenticationMethod {
|
||||
case NSURLAuthenticationMethodClientCertificate: handleClientCertificate(completion: completionHandler)
|
||||
default: completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleClientCertificate(
|
||||
completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassIdentity,
|
||||
kSecAttrLabel as String: CLIENT_CERT_LABEL,
|
||||
kSecReturnRef as String: true,
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
if status == errSecSuccess, let identity = item {
|
||||
let credential = URLCredential(identity: identity as! SecIdentity,
|
||||
certificates: nil,
|
||||
persistence: .forSession)
|
||||
return completion(.useCredential, credential)
|
||||
}
|
||||
completion(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
7
mobile/ios/Runner/Images/ImageProcessing.swift
Normal file
7
mobile/ios/Runner/Images/ImageProcessing.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
enum ImageProcessing {
|
||||
static let queue = DispatchQueue(label: "thumbnail.processing", qos: .userInitiated, attributes: .concurrent)
|
||||
static let semaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
|
||||
static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
||||
}
|
||||
@@ -34,7 +34,6 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
||||
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
||||
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
|
||||
|
||||
private static var rgbaFormat = vImage_CGImageFormat(
|
||||
bitsPerComponent: 8,
|
||||
@@ -44,8 +43,6 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
renderingIntent: .defaultIntent
|
||||
)!
|
||||
private static var requests = [Int64: LocalImageRequest]()
|
||||
private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
||||
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
|
||||
private static let assetCache = {
|
||||
let assetCache = NSCache<NSString, PHAsset>()
|
||||
assetCache.countLimit = 10000
|
||||
@@ -53,7 +50,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}()
|
||||
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||
Self.processingQueue.async {
|
||||
ImageProcessing.queue.async {
|
||||
guard let data = Data(base64Encoded: thumbhash)
|
||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||
|
||||
@@ -71,16 +68,16 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
let request = LocalImageRequest(callback: completion)
|
||||
let item = DispatchWorkItem {
|
||||
if request.isCancelled {
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
Self.concurrencySemaphore.wait()
|
||||
ImageProcessing.semaphore.wait()
|
||||
defer {
|
||||
Self.concurrencySemaphore.signal()
|
||||
ImageProcessing.semaphore.signal()
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let asset = Self.requestAsset(assetId: assetId)
|
||||
@@ -91,7 +88,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
var image: UIImage?
|
||||
@@ -106,7 +103,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
)
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let image = image,
|
||||
@@ -116,7 +113,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
do {
|
||||
@@ -124,7 +121,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return completion(Self.cancelledResult)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
request.callback(.success([
|
||||
@@ -133,7 +130,6 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
"height": Int64(buffer.height),
|
||||
"rowBytes": Int64(buffer.rowBytes)
|
||||
]))
|
||||
print("Successful response for \(requestId)")
|
||||
Self.remove(requestId: requestId)
|
||||
} catch {
|
||||
Self.remove(requestId: requestId)
|
||||
@@ -143,7 +139,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
|
||||
request.workItem = item
|
||||
Self.add(requestId: requestId, request: request)
|
||||
Self.processingQueue.async(execute: item)
|
||||
ImageProcessing.queue.async(execute: item)
|
||||
}
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
@@ -164,7 +160,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
request.isCancelled = true
|
||||
guard let item = request.workItem else { return }
|
||||
if item.isCancelled {
|
||||
cancelQueue.async { request.callback(Self.cancelledResult) }
|
||||
cancelQueue.async { request.callback(ImageProcessing.cancelledResult) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,64 +7,18 @@ class RemoteImageRequest {
|
||||
weak var task: URLSessionDataTask?
|
||||
let id: Int64
|
||||
var isCancelled = false
|
||||
var data: CFMutableData?
|
||||
let completion: (Result<[String: Int64]?, any Error>) -> Void
|
||||
|
||||
init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
self.id = id
|
||||
self.task = task
|
||||
self.data = nil
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||
private static let delegate = RemoteImageApiDelegate()
|
||||
static let session = {
|
||||
let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent("thumbnails", isDirectory: true)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||
config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"]
|
||||
try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
|
||||
config.urlCache = URLCache(
|
||||
memoryCapacity: 0,
|
||||
diskCapacity: 1 << 30,
|
||||
directory: cacheDir
|
||||
)
|
||||
config.httpMaximumConnectionsPerHost = 64
|
||||
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
||||
}()
|
||||
|
||||
func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
var urlRequest = URLRequest(url: URL(string: url)!)
|
||||
for (key, value) in headers {
|
||||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
let task = Self.session.dataTask(with: urlRequest)
|
||||
|
||||
let imageRequest = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
||||
Self.delegate.add(taskId: task.taskIdentifier, request: imageRequest)
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
Self.delegate.cancel(requestId: requestId)
|
||||
}
|
||||
|
||||
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
||||
Task {
|
||||
let cache = Self.session.configuration.urlCache!
|
||||
let cacheSize = Int64(cache.currentDiskUsage)
|
||||
cache.removeAllCachedResponses()
|
||||
completion(.success(cacheSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated, attributes: .concurrent)
|
||||
private static var lock = os_unfair_lock()
|
||||
private static var requests = [Int64: RemoteImageRequest]()
|
||||
private static var rgbaFormat = vImage_CGImageFormat(
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 32,
|
||||
@@ -72,9 +26,6 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||
renderingIntent: .perceptual
|
||||
)!
|
||||
private static var requestByTaskId = [Int: RemoteImageRequest]()
|
||||
private static var taskIdByRequestId = [Int64: Int]()
|
||||
private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
||||
private static let decodeOptions = [
|
||||
kCGImageSourceShouldCache: false,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
@@ -82,105 +33,103 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true
|
||||
] as CFDictionary
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession, dataTask: URLSessionDataTask,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
||||
) {
|
||||
guard let request = get(taskId: dataTask.taskIdentifier)
|
||||
else {
|
||||
return completionHandler(.cancel)
|
||||
func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
var urlRequest = URLRequest(url: URL(string: url)!)
|
||||
urlRequest.cachePolicy = .returnCacheDataElseLoad
|
||||
for (key, value) in headers {
|
||||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
let capacity = max(Int(response.expectedContentLength), 0)
|
||||
request.data = CFDataCreateMutable(nil, capacity)
|
||||
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
|
||||
didReceive data: Data) {
|
||||
guard let request = get(taskId: dataTask.taskIdentifier) else { return }
|
||||
|
||||
data.withUnsafeBytes { bytes in
|
||||
CFDataAppendBytes(request.data, bytes.bindMemory(to: UInt8.self).baseAddress, data.count)
|
||||
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
|
||||
Self.handleCompletion(requestId: requestId, data: data, response: response, error: error)
|
||||
}
|
||||
|
||||
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
||||
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
Self.requests[requestId] = request
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask,
|
||||
didCompleteWithError error: Error?) {
|
||||
guard let request = get(taskId: task.taskIdentifier) else { return }
|
||||
|
||||
defer { remove(taskId: task.taskIdentifier, requestId: request.id) }
|
||||
private static func handleCompletion(requestId: Int64, data: Data?, response: URLResponse?, error: Error?) {
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
guard let request = requests[requestId] else {
|
||||
return os_unfair_lock_unlock(&Self.lock)
|
||||
}
|
||||
requests[requestId] = nil
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
if let error = error {
|
||||
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
|
||||
return request.completion(Self.cancelledResult)
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
return request.completion(.failure(error))
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(Self.cancelledResult)
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let data = request.data else {
|
||||
guard let data = data else {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
|
||||
}
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data, nil),
|
||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, Self.decodeOptions) else {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(Self.cancelledResult)
|
||||
}
|
||||
|
||||
do {
|
||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)
|
||||
ImageProcessing.queue.async {
|
||||
ImageProcessing.semaphore.wait()
|
||||
defer { ImageProcessing.semaphore.signal() }
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return request.completion(Self.cancelledResult)
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
request.completion(
|
||||
.success([
|
||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||
"width": Int64(buffer.width),
|
||||
"height": Int64(buffer.height),
|
||||
"rowBytes": Int64(buffer.rowBytes),
|
||||
]))
|
||||
} catch {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil)))
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
|
||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||
}
|
||||
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
do {
|
||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat)
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
request.completion(
|
||||
.success([
|
||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||
"width": Int64(buffer.width),
|
||||
"height": Int64(buffer.height),
|
||||
"rowBytes": Int64(buffer.rowBytes),
|
||||
]))
|
||||
} catch {
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always) func get(taskId: Int) -> RemoteImageRequest? {
|
||||
Self.requestQueue.sync { Self.requestByTaskId[taskId] }
|
||||
}
|
||||
|
||||
@inline(__always) func add(taskId: Int, request: RemoteImageRequest) -> Void {
|
||||
Self.requestQueue.async(flags: .barrier) {
|
||||
Self.requestByTaskId[taskId] = request
|
||||
Self.taskIdByRequestId[request.id] = taskId
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always) func remove(taskId: Int, requestId: Int64) -> Void {
|
||||
Self.requestQueue.async(flags: .barrier) {
|
||||
Self.taskIdByRequestId[requestId] = nil
|
||||
Self.requestByTaskId[taskId] = nil
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always) func cancel(requestId: Int64) -> Void {
|
||||
guard let request: RemoteImageRequest = (Self.requestQueue.sync {
|
||||
guard let taskId = Self.taskIdByRequestId[requestId] else { return nil }
|
||||
return Self.requestByTaskId[taskId]
|
||||
}) else { return }
|
||||
func cancelRequest(requestId: Int64) {
|
||||
os_unfair_lock_lock(&Self.lock)
|
||||
let request = Self.requests[requestId]
|
||||
os_unfair_lock_unlock(&Self.lock)
|
||||
|
||||
guard let request = request else { return }
|
||||
request.isCancelled = true
|
||||
request.task?.cancel()
|
||||
}
|
||||
|
||||
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
||||
Task {
|
||||
let cache = URLSessionManager.shared.session.configuration.urlCache!
|
||||
let cacheSize = Int64(cache.currentDiskUsage)
|
||||
cache.removeAllCachedResponses()
|
||||
completion(.success(cacheSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
HttpSSLOptions.apply(applyNative: false);
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
await Future.wait(
|
||||
[
|
||||
|
||||
@@ -20,7 +20,7 @@ enum VersionStatus {
|
||||
|
||||
class ServerInfo {
|
||||
final ServerVersion serverVersion;
|
||||
final ServerVersion latestVersion;
|
||||
final ServerVersion? latestVersion;
|
||||
final ServerFeatures serverFeatures;
|
||||
final ServerConfig serverConfig;
|
||||
final ServerDiskInfo serverDiskInfo;
|
||||
|
||||
232
mobile/lib/platform/network_api.g.dart
generated
Normal file
232
mobile/lib/platform/network_api.g.dart
generated
Normal file
@@ -0,0 +1,232 @@
|
||||
// Autogenerated from Pigeon (v26.0.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".',
|
||||
);
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
class ClientCertData {
|
||||
ClientCertData({required this.data, required this.password});
|
||||
|
||||
Uint8List data;
|
||||
|
||||
String password;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[data, password];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static ClientCertData decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return ClientCertData(data: result[0]! as Uint8List, password: result[1]! as String);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ClientCertData || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class ClientCertPrompt {
|
||||
ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm});
|
||||
|
||||
String title;
|
||||
|
||||
String message;
|
||||
|
||||
String cancel;
|
||||
|
||||
String confirm;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[title, message, cancel, confirm];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static ClientCertPrompt decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return ClientCertPrompt(
|
||||
title: result[0]! as String,
|
||||
message: result[1]! as String,
|
||||
cancel: result[2]! as String,
|
||||
confirm: result[3]! as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ClientCertPrompt || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is ClientCertData) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is ClientCertPrompt) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
return ClientCertData.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return ClientCertPrompt.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkApi {
|
||||
/// Constructor for [NetworkApi]. 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.
|
||||
NetworkApi({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<void> addCertificate(ClientCertData clientData) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[clientData]);
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ClientCertData> selectCertificate(ClientCertPrompt promptText) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[promptText]);
|
||||
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 ClientCertData?)!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeCertificate() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/platform/local_image_api.g.dart';
|
||||
import 'package:immich_mobile/platform/network_api.g.dart';
|
||||
import 'package:immich_mobile/platform/remote_image_api.g.dart';
|
||||
|
||||
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||
@@ -20,3 +21,5 @@ final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi
|
||||
final localImageApi = LocalImageApi();
|
||||
|
||||
final remoteImageApi = RemoteImageApi();
|
||||
|
||||
final networkApi = NetworkApi();
|
||||
|
||||
@@ -15,7 +15,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
: super(
|
||||
const ServerInfo(
|
||||
serverVersion: ServerVersion(major: 0, minor: 0, patch: 0),
|
||||
latestVersion: ServerVersion(major: 0, minor: 0, patch: 0),
|
||||
latestVersion: null,
|
||||
serverFeatures: ServerFeatures(map: true, trash: true, oauthEnabled: false, passwordLogin: true),
|
||||
serverConfig: ServerConfig(
|
||||
trashDays: 30,
|
||||
@@ -43,7 +43,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
try {
|
||||
final serverVersion = await _serverInfoService.getServerVersion();
|
||||
|
||||
// using isClientOutOfDate since that will show to users reguardless of if they are an admin
|
||||
// using isClientOutOfDate since that will show to users regardless of if they are an admin
|
||||
if (serverVersion == null) {
|
||||
state = state.copyWith(versionStatus: VersionStatus.error);
|
||||
return;
|
||||
@@ -76,7 +76,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
state = state.copyWith(versionStatus: VersionStatus.upToDate);
|
||||
}
|
||||
|
||||
handleReleaseInfo(ServerVersion serverVersion, ServerVersion latestVersion) {
|
||||
handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) {
|
||||
// Update local server version
|
||||
_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class HttpSSLOptions {
|
||||
static const MethodChannel _channel = MethodChannel('immich/httpSSLOptions');
|
||||
|
||||
static void apply({bool applyNative = true}) {
|
||||
static void apply() {
|
||||
AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert;
|
||||
bool allowSelfSignedSSLCert = Store.get(setting.storeKey as StoreKey<bool>, setting.defaultValue);
|
||||
_apply(allowSelfSignedSSLCert, applyNative: applyNative);
|
||||
return _apply(allowSelfSignedSSLCert);
|
||||
}
|
||||
|
||||
static void applyFromSettings(bool newValue) {
|
||||
_apply(newValue);
|
||||
}
|
||||
static void applyFromSettings(bool newValue) => _apply(newValue);
|
||||
|
||||
static void _apply(bool allowSelfSignedSSLCert, {bool applyNative = true}) {
|
||||
static void _apply(bool allowSelfSignedSSLCert) {
|
||||
String? serverHost;
|
||||
if (allowSelfSignedSSLCert && Store.tryGet(StoreKey.currentUser) != null) {
|
||||
serverHost = Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host;
|
||||
@@ -29,14 +23,5 @@ class HttpSSLOptions {
|
||||
SSLClientCertStoreVal? clientCert = SSLClientCertStoreVal.load();
|
||||
|
||||
HttpOverrides.global = HttpSSLCertOverride(allowSelfSignedSSLCert, serverHost, clientCert);
|
||||
|
||||
if (applyNative && Platform.isAndroid) {
|
||||
_channel
|
||||
.invokeMethod("apply", [allowSelfSignedSSLCert, serverHost, clientCert?.data, clientCert?.password])
|
||||
.onError<PlatformException>((e, _) {
|
||||
final log = Logger("HttpSSLOptions");
|
||||
log.severe('Failed to set SSL options', e.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||
Logger log = Logger("IsolateLogger");
|
||||
|
||||
try {
|
||||
HttpSSLOptions.apply(applyNative: false);
|
||||
HttpSSLOptions.apply();
|
||||
result = await computation(ref);
|
||||
} on CanceledError {
|
||||
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
|
||||
|
||||
@@ -23,6 +23,8 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/platform/network_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
@@ -32,7 +34,7 @@ import 'package:isar/isar.dart';
|
||||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
const int targetVersion = 20;
|
||||
const int targetVersion = 21;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||
@@ -91,6 +93,13 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
await _syncLocalAlbumIsIosSharedAlbum(drift);
|
||||
}
|
||||
|
||||
if (version < 21) {
|
||||
final certData = SSLClientCertStoreVal.load();
|
||||
if (certData != null) {
|
||||
await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (targetVersion >= 12) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
|
||||
@@ -170,50 +170,52 @@ class AppBarServerInfo extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
||||
if (serverInfoState.latestVersion != null) ...[
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
||||
),
|
||||
Text(
|
||||
"latest_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: context.textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"latest_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: context.textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
serverInfoState.latestVersion.major > 0
|
||||
? "${serverInfoState.latestVersion.major}.${serverInfoState.latestVersion.minor}.${serverInfoState.latestVersion.patch}"
|
||||
: "--",
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
serverInfoState.latestVersion!.major > 0
|
||||
? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}"
|
||||
: "--",
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -414,6 +414,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
keyboardAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
autoCorrect: false,
|
||||
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:immich_mobile/platform/network_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class SslClientCertSettings extends StatefulWidget {
|
||||
const SslClientCertSettings({super.key, required this.isLoggedIn});
|
||||
@@ -20,10 +19,12 @@ class SslClientCertSettings extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SslClientCertSettingsState extends State<SslClientCertSettings> {
|
||||
_SslClientCertSettingsState() : isCertExist = SSLClientCertStoreVal.load() != null;
|
||||
final _log = Logger("SslClientCertSettings");
|
||||
|
||||
bool isCertExist;
|
||||
|
||||
_SslClientCertSettingsState() : isCertExist = SSLClientCertStoreVal.load() != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
@@ -41,16 +42,12 @@ class _SslClientCertSettingsState extends State<SslClientCertSettings> {
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(onPressed: widget.isLoggedIn ? null : importCert, child: Text("client_cert_import".tr())),
|
||||
ElevatedButton(
|
||||
onPressed: widget.isLoggedIn ? null : () => importCert(context),
|
||||
child: Text("client_cert_import".tr()),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
ElevatedButton(
|
||||
onPressed: widget.isLoggedIn || !isCertExist ? null : () async => await removeCert(context),
|
||||
onPressed: widget.isLoggedIn || !isCertExist ? null : removeCert,
|
||||
child: Text("remove".tr()),
|
||||
),
|
||||
],
|
||||
@@ -60,71 +57,48 @@ class _SslClientCertSettingsState extends State<SslClientCertSettings> {
|
||||
);
|
||||
}
|
||||
|
||||
void showMessage(BuildContext context, String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
content: Text(message),
|
||||
actions: [TextButton(onPressed: () => ctx.pop(), child: Text("client_cert_dialog_msg_confirm".tr()))],
|
||||
void showMessage(String message) {
|
||||
context.showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: Text(message, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> storeCert(BuildContext context, Uint8List data, String? password) async {
|
||||
if (password != null && password.isEmpty) {
|
||||
password = null;
|
||||
}
|
||||
final cert = SSLClientCertStoreVal(data, password);
|
||||
// Test whether the certificate is valid
|
||||
final isCertValid = HttpSSLCertOverride.setClientCert(SecurityContext(withTrustedRoots: true), cert);
|
||||
if (!isCertValid) {
|
||||
showMessage(context, "client_cert_invalid_msg".tr());
|
||||
return;
|
||||
}
|
||||
await cert.save();
|
||||
HttpSSLOptions.apply();
|
||||
setState(() => isCertExist = true);
|
||||
showMessage(context, "client_cert_import_success_msg".tr());
|
||||
}
|
||||
|
||||
void setPassword(BuildContext context, Uint8List data) {
|
||||
final password = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
content: TextField(
|
||||
controller: password,
|
||||
obscureText: true,
|
||||
obscuringCharacter: "*",
|
||||
decoration: InputDecoration(hintText: "client_cert_enter_password".tr()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async => {ctx.pop(), await storeCert(context, data, password.text)},
|
||||
child: Text("client_cert_dialog_msg_confirm".tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> importCert(BuildContext ctx) async {
|
||||
FilePickerResult? res = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['p12', 'pfx'],
|
||||
);
|
||||
if (res != null) {
|
||||
File file = File(res.files.single.path!);
|
||||
final bytes = await file.readAsBytes();
|
||||
setPassword(ctx, bytes);
|
||||
Future<void> importCert() async {
|
||||
try {
|
||||
final styling = ClientCertPrompt(
|
||||
title: "client_cert_password_title".tr(),
|
||||
message: "client_cert_password_message".tr(),
|
||||
cancel: "cancel".tr(),
|
||||
confirm: "confirm".tr(),
|
||||
);
|
||||
final cert = await networkApi.selectCertificate(styling);
|
||||
await SSLClientCertStoreVal(cert.data, cert.password).save();
|
||||
HttpSSLOptions.apply();
|
||||
setState(() => isCertExist = true);
|
||||
showMessage("client_cert_import_success_msg".tr());
|
||||
} catch (e) {
|
||||
if (_isCancellation(e)) return;
|
||||
_log.severe("Error importing client cert", e);
|
||||
showMessage("client_cert_invalid_msg".tr());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeCert(BuildContext context) async {
|
||||
await SSLClientCertStoreVal.delete();
|
||||
HttpSSLOptions.apply();
|
||||
setState(() => isCertExist = false);
|
||||
showMessage(context, "client_cert_remove_msg".tr());
|
||||
Future<void> removeCert() async {
|
||||
try {
|
||||
await networkApi.removeCertificate();
|
||||
await SSLClientCertStoreVal.delete();
|
||||
HttpSSLOptions.apply();
|
||||
setState(() => isCertExist = false);
|
||||
showMessage("client_cert_remove_msg".tr());
|
||||
} catch (e) {
|
||||
if (_isCancellation(e)) return;
|
||||
_log.severe("Error removing client cert", e);
|
||||
showMessage("client_cert_invalid_msg".tr());
|
||||
}
|
||||
}
|
||||
|
||||
bool _isCancellation(Object e) => e is PlatformException && e.code.toLowerCase().contains("cancel");
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ pigeon:
|
||||
dart run pigeon --input pigeon/background_worker_api.dart
|
||||
dart run pigeon --input pigeon/background_worker_lock_api.dart
|
||||
dart run pigeon --input pigeon/connectivity_api.dart
|
||||
dart run pigeon --input pigeon/network_api.dart
|
||||
dart format lib/platform/native_sync_api.g.dart
|
||||
dart format lib/platform/local_image_api.g.dart
|
||||
dart format lib/platform/remote_image_api.g.dart
|
||||
dart format lib/platform/background_worker_api.g.dart
|
||||
dart format lib/platform/background_worker_lock_api.g.dart
|
||||
dart format lib/platform/connectivity_api.g.dart
|
||||
dart format lib/platform/network_api.g.dart
|
||||
|
||||
watch:
|
||||
dart run build_runner watch --delete-conflicting-outputs
|
||||
|
||||
14
mobile/openapi/lib/api/activities_api.dart
generated
14
mobile/openapi/lib/api/activities_api.dart
generated
@@ -130,14 +130,19 @@ class ActivitiesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Asset ID (if activity is for an asset)
|
||||
///
|
||||
/// * [ReactionLevel] level:
|
||||
/// Filter by activity level
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
/// Filter by activity type
|
||||
///
|
||||
/// * [String] userId:
|
||||
/// Filter by user ID
|
||||
Future<Response> getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/activities';
|
||||
@@ -184,14 +189,19 @@ class ActivitiesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Asset ID (if activity is for an asset)
|
||||
///
|
||||
/// * [ReactionLevel] level:
|
||||
/// Filter by activity level
|
||||
///
|
||||
/// * [ReactionType] type:
|
||||
/// Filter by activity type
|
||||
///
|
||||
/// * [String] userId:
|
||||
/// Filter by user ID
|
||||
Future<List<ActivityResponseDto>?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async {
|
||||
final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -219,8 +229,10 @@ class ActivitiesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Asset ID (if activity is for an asset)
|
||||
Future<Response> getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/activities/statistics';
|
||||
@@ -258,8 +270,10 @@ class ActivitiesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId (required):
|
||||
/// Album ID
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Asset ID (if activity is for an asset)
|
||||
Future<ActivityStatisticsResponseDto?> getActivityStatistics(String albumId, { String? assetId, }) async {
|
||||
final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
8
mobile/openapi/lib/api/albums_api.dart
generated
8
mobile/openapi/lib/api/albums_api.dart
generated
@@ -347,6 +347,7 @@ class AlbumsApi {
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [bool] withoutAssets:
|
||||
/// Exclude assets from response
|
||||
Future<Response> getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums/{id}'
|
||||
@@ -396,6 +397,7 @@ class AlbumsApi {
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [bool] withoutAssets:
|
||||
/// Exclude assets from response
|
||||
Future<AlbumResponseDto?> getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||
final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -468,9 +470,10 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
|
||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||
///
|
||||
/// * [bool] shared:
|
||||
/// Filter by shared status: true = only shared, false = only own, undefined = all
|
||||
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums';
|
||||
@@ -510,9 +513,10 @@ class AlbumsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] assetId:
|
||||
/// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
|
||||
/// Filter albums containing this asset ID (ignores shared parameter)
|
||||
///
|
||||
/// * [bool] shared:
|
||||
/// Filter by shared status: true = only shared, false = only own, undefined = all
|
||||
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? shared, }) async {
|
||||
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
62
mobile/openapi/lib/api/assets_api.dart
generated
62
mobile/openapi/lib/api/assets_api.dart
generated
@@ -185,8 +185,10 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Asset ID
|
||||
///
|
||||
/// * [String] key (required):
|
||||
/// Metadata key
|
||||
Future<Response> deleteAssetMetadataWithHttpInfo(String id, String key,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/metadata/{key}'
|
||||
@@ -221,8 +223,10 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Asset ID
|
||||
///
|
||||
/// * [String] key (required):
|
||||
/// Metadata key
|
||||
Future<void> deleteAssetMetadata(String id, String key,) async {
|
||||
final response = await deleteAssetMetadataWithHttpInfo(id, key,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -337,6 +341,7 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
/// Return edited asset if available
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
@@ -386,6 +391,7 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
/// Return edited asset if available
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
@@ -475,6 +481,7 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
Future<Response> getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/device/{deviceId}'
|
||||
@@ -508,6 +515,7 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
Future<List<String>?> getAllUserAssetsByDeviceId(String deviceId,) async {
|
||||
final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -724,8 +732,10 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Asset ID
|
||||
///
|
||||
/// * [String] key (required):
|
||||
/// Metadata key
|
||||
Future<Response> getAssetMetadataByKeyWithHttpInfo(String id, String key,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/metadata/{key}'
|
||||
@@ -760,8 +770,10 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Asset ID
|
||||
///
|
||||
/// * [String] key (required):
|
||||
/// Metadata key
|
||||
Future<AssetMetadataResponseDto?> getAssetMetadataByKey(String id, String key,) async {
|
||||
final response = await getAssetMetadataByKeyWithHttpInfo(id, key,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -846,10 +858,13 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Filter by trash status
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
Future<Response> getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/statistics';
|
||||
@@ -892,10 +907,13 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Filter by trash status
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -920,6 +938,7 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
@@ -956,6 +975,7 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -1106,22 +1126,29 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/original'
|
||||
@@ -1198,22 +1225,29 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -1518,14 +1552,19 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
@@ -1535,18 +1574,25 @@ class AssetsApi {
|
||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Mark as favorite
|
||||
///
|
||||
/// * [String] livePhotoVideoId:
|
||||
/// Live photo video ID
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||
/// Asset metadata items
|
||||
///
|
||||
/// * [MultipartFile] sidecarData:
|
||||
/// Sidecar file data
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Asset visibility
|
||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets';
|
||||
@@ -1645,14 +1691,19 @@ class AssetsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
@@ -1662,18 +1713,25 @@ class AssetsApi {
|
||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Mark as favorite
|
||||
///
|
||||
/// * [String] livePhotoVideoId:
|
||||
/// Live photo video ID
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||
/// Asset metadata items
|
||||
///
|
||||
/// * [MultipartFile] sidecarData:
|
||||
/// Sidecar file data
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Asset visibility
|
||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -1700,10 +1758,12 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
/// Return edited asset if available
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [AssetMediaSize] size:
|
||||
/// Asset media size
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
@@ -1754,10 +1814,12 @@ class AssetsApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
/// Return edited asset if available
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [AssetMediaSize] size:
|
||||
/// Asset media size
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<MultipartFile?> viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
|
||||
20
mobile/openapi/lib/api/deprecated_api.dart
generated
20
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -82,6 +82,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
Future<Response> getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/device/{deviceId}'
|
||||
@@ -115,6 +116,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
Future<List<String>?> getAllUserAssetsByDeviceId(String deviceId,) async {
|
||||
final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -305,6 +307,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
@@ -341,6 +344,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -370,22 +374,29 @@ class DeprecatedApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/original'
|
||||
@@ -462,22 +473,29 @@ class DeprecatedApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [MultipartFile] assetData (required):
|
||||
/// Asset file data
|
||||
///
|
||||
/// * [String] deviceAssetId (required):
|
||||
/// Device asset ID
|
||||
///
|
||||
/// * [String] deviceId (required):
|
||||
/// Device ID
|
||||
///
|
||||
/// * [DateTime] fileCreatedAt (required):
|
||||
/// File creation date
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
/// File modification date
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
||||
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -502,6 +520,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<Response> runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
@@ -537,6 +556,7 @@ class DeprecatedApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<QueueResponseLegacyDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
|
||||
2
mobile/openapi/lib/api/faces_api.dart
generated
2
mobile/openapi/lib/api/faces_api.dart
generated
@@ -126,6 +126,7 @@ class FacesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Face ID
|
||||
Future<Response> getFacesWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/faces';
|
||||
@@ -160,6 +161,7 @@ class FacesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
/// Face ID
|
||||
Future<List<AssetFaceResponseDto>?> getFaces(String id,) async {
|
||||
final response = await getFacesWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
2
mobile/openapi/lib/api/jobs_api.dart
generated
2
mobile/openapi/lib/api/jobs_api.dart
generated
@@ -121,6 +121,7 @@ class JobsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<Response> runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
@@ -156,6 +157,7 @@ class JobsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<QueueResponseLegacyDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
|
||||
16
mobile/openapi/lib/api/map_api.dart
generated
16
mobile/openapi/lib/api/map_api.dart
generated
@@ -25,16 +25,22 @@ class MapApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] fileCreatedAfter:
|
||||
/// Filter assets created after this date
|
||||
///
|
||||
/// * [DateTime] fileCreatedBefore:
|
||||
/// Filter assets created before this date
|
||||
///
|
||||
/// * [bool] isArchived:
|
||||
/// Filter by archived status
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include partner assets
|
||||
///
|
||||
/// * [bool] withSharedAlbums:
|
||||
/// Include shared album assets
|
||||
Future<Response> getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/markers';
|
||||
@@ -86,16 +92,22 @@ class MapApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] fileCreatedAfter:
|
||||
/// Filter assets created after this date
|
||||
///
|
||||
/// * [DateTime] fileCreatedBefore:
|
||||
/// Filter assets created before this date
|
||||
///
|
||||
/// * [bool] isArchived:
|
||||
/// Filter by archived status
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include partner assets
|
||||
///
|
||||
/// * [bool] withSharedAlbums:
|
||||
/// Include shared album assets
|
||||
Future<List<MapMarkerResponseDto>?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
||||
final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -123,8 +135,10 @@ class MapApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [double] lat (required):
|
||||
/// Latitude (-90 to 90)
|
||||
///
|
||||
/// * [double] lon (required):
|
||||
/// Longitude (-180 to 180)
|
||||
Future<Response> reverseGeocodeWithHttpInfo(double lat, double lon,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/reverse-geocode';
|
||||
@@ -160,8 +174,10 @@ class MapApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [double] lat (required):
|
||||
/// Latitude (-90 to 90)
|
||||
///
|
||||
/// * [double] lon (required):
|
||||
/// Longitude (-180 to 180)
|
||||
Future<List<MapReverseGeocodeResponseDto>?> reverseGeocode(double lat, double lon,) async {
|
||||
final response = await reverseGeocodeWithHttpInfo(lat, lon,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
20
mobile/openapi/lib/api/memories_api.dart
generated
20
mobile/openapi/lib/api/memories_api.dart
generated
@@ -251,17 +251,22 @@ class MemoriesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
/// Filter by date
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
/// Filter by saved status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Include trashed memories
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
/// Sort order
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
/// Memory type
|
||||
Future<Response> memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/memories/statistics';
|
||||
@@ -313,17 +318,22 @@ class MemoriesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
/// Filter by date
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
/// Filter by saved status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Include trashed memories
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
/// Sort order
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
/// Memory type
|
||||
Future<MemoryStatisticsResponseDto?> memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -412,17 +422,22 @@ class MemoriesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
/// Filter by date
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
/// Filter by saved status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Include trashed memories
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
/// Sort order
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
/// Memory type
|
||||
Future<Response> searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/memories';
|
||||
@@ -474,17 +489,22 @@ class MemoriesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
/// Filter by date
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
/// Filter by saved status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Include trashed memories
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
/// Sort order
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
/// Memory type
|
||||
Future<List<MemoryResponseDto>?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
8
mobile/openapi/lib/api/notifications_api.dart
generated
8
mobile/openapi/lib/api/notifications_api.dart
generated
@@ -179,12 +179,16 @@ class NotificationsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Filter by notification ID
|
||||
///
|
||||
/// * [NotificationLevel] level:
|
||||
/// Filter by notification level
|
||||
///
|
||||
/// * [NotificationType] type:
|
||||
/// Filter by notification type
|
||||
///
|
||||
/// * [bool] unread:
|
||||
/// Filter by unread status
|
||||
Future<Response> getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/notifications';
|
||||
@@ -230,12 +234,16 @@ class NotificationsApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Filter by notification ID
|
||||
///
|
||||
/// * [NotificationLevel] level:
|
||||
/// Filter by notification level
|
||||
///
|
||||
/// * [NotificationType] type:
|
||||
/// Filter by notification type
|
||||
///
|
||||
/// * [bool] unread:
|
||||
/// Filter by unread status
|
||||
Future<List<NotificationDto>?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||
final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
2
mobile/openapi/lib/api/partners_api.dart
generated
2
mobile/openapi/lib/api/partners_api.dart
generated
@@ -138,6 +138,7 @@ class PartnersApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PartnerDirection] direction (required):
|
||||
/// Partner direction
|
||||
Future<Response> getPartnersWithHttpInfo(PartnerDirection direction,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/partners';
|
||||
@@ -172,6 +173,7 @@ class PartnersApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PartnerDirection] direction (required):
|
||||
/// Partner direction
|
||||
Future<List<PartnerResponseDto>?> getPartners(PartnerDirection direction,) async {
|
||||
final response = await getPartnersWithHttpInfo(direction,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
6
mobile/openapi/lib/api/people_api.dart
generated
6
mobile/openapi/lib/api/people_api.dart
generated
@@ -178,8 +178,10 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] closestAssetId:
|
||||
/// Closest asset ID for similarity search
|
||||
///
|
||||
/// * [String] closestPersonId:
|
||||
/// Closest person ID for similarity search
|
||||
///
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
@@ -188,6 +190,7 @@ class PeopleApi {
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
/// Include hidden people
|
||||
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/people';
|
||||
@@ -236,8 +239,10 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] closestAssetId:
|
||||
/// Closest asset ID for similarity search
|
||||
///
|
||||
/// * [String] closestPersonId:
|
||||
/// Closest person ID for similarity search
|
||||
///
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
@@ -246,6 +251,7 @@ class PeopleApi {
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
/// Include hidden people
|
||||
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
||||
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
10
mobile/openapi/lib/api/queues_api.dart
generated
10
mobile/openapi/lib/api/queues_api.dart
generated
@@ -25,6 +25,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueDeleteDto] queueDeleteDto (required):
|
||||
Future<Response> emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async {
|
||||
@@ -60,6 +61,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueDeleteDto] queueDeleteDto (required):
|
||||
Future<void> emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async {
|
||||
@@ -78,6 +80,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
Future<Response> getQueueWithHttpInfo(QueueName name,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/queues/{name}'
|
||||
@@ -111,6 +114,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
Future<QueueResponseDto?> getQueue(QueueName name,) async {
|
||||
final response = await getQueueWithHttpInfo(name,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -135,8 +139,10 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [List<QueueJobStatus>] status:
|
||||
/// Filter jobs by status
|
||||
Future<Response> getQueueJobsWithHttpInfo(QueueName name, { List<QueueJobStatus>? status, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/queues/{name}/jobs'
|
||||
@@ -174,8 +180,10 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [List<QueueJobStatus>] status:
|
||||
/// Filter jobs by status
|
||||
Future<List<QueueJobResponseDto>?> getQueueJobs(QueueName name, { List<QueueJobStatus>? status, }) async {
|
||||
final response = await getQueueJobsWithHttpInfo(name, status: status, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -254,6 +262,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueUpdateDto] queueUpdateDto (required):
|
||||
Future<Response> updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async {
|
||||
@@ -289,6 +298,7 @@ class QueuesApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [QueueName] name (required):
|
||||
/// Queue name
|
||||
///
|
||||
/// * [QueueUpdateDto] queueUpdateDto (required):
|
||||
Future<QueueResponseDto?> updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async {
|
||||
|
||||
84
mobile/openapi/lib/api/search_api.dart
generated
84
mobile/openapi/lib/api/search_api.dart
generated
@@ -127,18 +127,25 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SearchSuggestionType] type (required):
|
||||
/// Suggestion type
|
||||
///
|
||||
/// * [String] country:
|
||||
/// Filter by country
|
||||
///
|
||||
/// * [bool] includeNull:
|
||||
/// Include null values in suggestions
|
||||
///
|
||||
/// * [String] lensModel:
|
||||
/// Filter by lens model
|
||||
///
|
||||
/// * [String] make:
|
||||
/// Filter by camera make
|
||||
///
|
||||
/// * [String] model:
|
||||
/// Filter by camera model
|
||||
///
|
||||
/// * [String] state:
|
||||
/// Filter by state/province
|
||||
Future<Response> getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/suggestions';
|
||||
@@ -191,18 +198,25 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SearchSuggestionType] type (required):
|
||||
/// Suggestion type
|
||||
///
|
||||
/// * [String] country:
|
||||
/// Filter by country
|
||||
///
|
||||
/// * [bool] includeNull:
|
||||
/// Include null values in suggestions
|
||||
///
|
||||
/// * [String] lensModel:
|
||||
/// Filter by lens model
|
||||
///
|
||||
/// * [String] make:
|
||||
/// Filter by camera make
|
||||
///
|
||||
/// * [String] model:
|
||||
/// Filter by camera model
|
||||
///
|
||||
/// * [String] state:
|
||||
/// Filter by state/province
|
||||
Future<List<String>?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async {
|
||||
final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -342,68 +356,100 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [List<String>] albumIds:
|
||||
/// Filter by album IDs
|
||||
///
|
||||
/// * [String] city:
|
||||
/// Filter by city name
|
||||
///
|
||||
/// * [String] country:
|
||||
/// Filter by country name
|
||||
///
|
||||
/// * [DateTime] createdAfter:
|
||||
/// Filter by creation date (after)
|
||||
///
|
||||
/// * [DateTime] createdBefore:
|
||||
/// Filter by creation date (before)
|
||||
///
|
||||
/// * [String] deviceId:
|
||||
/// Device ID to filter by
|
||||
///
|
||||
/// * [bool] isEncoded:
|
||||
/// Filter by encoded status
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isMotion:
|
||||
/// Filter by motion photo status
|
||||
///
|
||||
/// * [bool] isNotInAlbum:
|
||||
/// Filter assets not in any album
|
||||
///
|
||||
/// * [bool] isOffline:
|
||||
/// Filter by offline status
|
||||
///
|
||||
/// * [String] lensModel:
|
||||
/// Filter by lens model
|
||||
///
|
||||
/// * [String] libraryId:
|
||||
/// Library ID to filter by
|
||||
///
|
||||
/// * [String] make:
|
||||
/// Filter by camera make
|
||||
///
|
||||
/// * [int] minFileSize:
|
||||
/// Minimum file size in bytes
|
||||
///
|
||||
/// * [String] model:
|
||||
/// Filter by camera model
|
||||
///
|
||||
/// * [String] ocr:
|
||||
/// Filter by OCR text content
|
||||
///
|
||||
/// * [List<String>] personIds:
|
||||
/// Filter by person IDs
|
||||
///
|
||||
/// * [num] rating:
|
||||
/// Filter by rating
|
||||
///
|
||||
/// * [num] size:
|
||||
/// Number of results to return
|
||||
///
|
||||
/// * [String] state:
|
||||
/// Filter by state/province name
|
||||
///
|
||||
/// * [List<String>] tagIds:
|
||||
/// Filter by tag IDs
|
||||
///
|
||||
/// * [DateTime] takenAfter:
|
||||
/// Filter by taken date (after)
|
||||
///
|
||||
/// * [DateTime] takenBefore:
|
||||
/// Filter by taken date (before)
|
||||
///
|
||||
/// * [DateTime] trashedAfter:
|
||||
/// Filter by trash date (after)
|
||||
///
|
||||
/// * [DateTime] trashedBefore:
|
||||
/// Filter by trash date (before)
|
||||
///
|
||||
/// * [AssetTypeEnum] type:
|
||||
/// Asset type filter
|
||||
///
|
||||
/// * [DateTime] updatedAfter:
|
||||
/// Filter by update date (after)
|
||||
///
|
||||
/// * [DateTime] updatedBefore:
|
||||
/// Filter by update date (before)
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
/// Include deleted assets
|
||||
///
|
||||
/// * [bool] withExif:
|
||||
/// Include EXIF data in response
|
||||
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/large-assets';
|
||||
@@ -533,68 +579,100 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [List<String>] albumIds:
|
||||
/// Filter by album IDs
|
||||
///
|
||||
/// * [String] city:
|
||||
/// Filter by city name
|
||||
///
|
||||
/// * [String] country:
|
||||
/// Filter by country name
|
||||
///
|
||||
/// * [DateTime] createdAfter:
|
||||
/// Filter by creation date (after)
|
||||
///
|
||||
/// * [DateTime] createdBefore:
|
||||
/// Filter by creation date (before)
|
||||
///
|
||||
/// * [String] deviceId:
|
||||
/// Device ID to filter by
|
||||
///
|
||||
/// * [bool] isEncoded:
|
||||
/// Filter by encoded status
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isMotion:
|
||||
/// Filter by motion photo status
|
||||
///
|
||||
/// * [bool] isNotInAlbum:
|
||||
/// Filter assets not in any album
|
||||
///
|
||||
/// * [bool] isOffline:
|
||||
/// Filter by offline status
|
||||
///
|
||||
/// * [String] lensModel:
|
||||
/// Filter by lens model
|
||||
///
|
||||
/// * [String] libraryId:
|
||||
/// Library ID to filter by
|
||||
///
|
||||
/// * [String] make:
|
||||
/// Filter by camera make
|
||||
///
|
||||
/// * [int] minFileSize:
|
||||
/// Minimum file size in bytes
|
||||
///
|
||||
/// * [String] model:
|
||||
/// Filter by camera model
|
||||
///
|
||||
/// * [String] ocr:
|
||||
/// Filter by OCR text content
|
||||
///
|
||||
/// * [List<String>] personIds:
|
||||
/// Filter by person IDs
|
||||
///
|
||||
/// * [num] rating:
|
||||
/// Filter by rating
|
||||
///
|
||||
/// * [num] size:
|
||||
/// Number of results to return
|
||||
///
|
||||
/// * [String] state:
|
||||
/// Filter by state/province name
|
||||
///
|
||||
/// * [List<String>] tagIds:
|
||||
/// Filter by tag IDs
|
||||
///
|
||||
/// * [DateTime] takenAfter:
|
||||
/// Filter by taken date (after)
|
||||
///
|
||||
/// * [DateTime] takenBefore:
|
||||
/// Filter by taken date (before)
|
||||
///
|
||||
/// * [DateTime] trashedAfter:
|
||||
/// Filter by trash date (after)
|
||||
///
|
||||
/// * [DateTime] trashedBefore:
|
||||
/// Filter by trash date (before)
|
||||
///
|
||||
/// * [AssetTypeEnum] type:
|
||||
/// Asset type filter
|
||||
///
|
||||
/// * [DateTime] updatedAfter:
|
||||
/// Filter by update date (after)
|
||||
///
|
||||
/// * [DateTime] updatedBefore:
|
||||
/// Filter by update date (before)
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
/// Include deleted assets
|
||||
///
|
||||
/// * [bool] withExif:
|
||||
/// Include EXIF data in response
|
||||
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -622,8 +700,10 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
/// Person name to search for
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
/// Include hidden people
|
||||
Future<Response> searchPersonWithHttpInfo(String name, { bool? withHidden, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/person';
|
||||
@@ -661,8 +741,10 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
/// Person name to search for
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
/// Include hidden people
|
||||
Future<List<PersonResponseDto>?> searchPerson(String name, { bool? withHidden, }) async {
|
||||
final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -690,6 +772,7 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
/// Place name to search for
|
||||
Future<Response> searchPlacesWithHttpInfo(String name,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/places';
|
||||
@@ -724,6 +807,7 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] name (required):
|
||||
/// Place name to search for
|
||||
Future<List<PlacesResponseDto>?> searchPlaces(String name,) async {
|
||||
final response = await searchPlacesWithHttpInfo(name,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
8
mobile/openapi/lib/api/shared_links_api.dart
generated
8
mobile/openapi/lib/api/shared_links_api.dart
generated
@@ -160,8 +160,10 @@ class SharedLinksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId:
|
||||
/// Filter by album ID
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Filter by shared link ID
|
||||
Future<Response> getAllSharedLinksWithHttpInfo({ String? albumId, String? id, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/shared-links';
|
||||
@@ -201,8 +203,10 @@ class SharedLinksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId:
|
||||
/// Filter by album ID
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Filter by shared link ID
|
||||
Future<List<SharedLinkResponseDto>?> getAllSharedLinks({ String? albumId, String? id, }) async {
|
||||
final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, id: id, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -232,10 +236,12 @@ class SharedLinksApi {
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] password:
|
||||
/// Link password
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] token:
|
||||
/// Access token
|
||||
Future<Response> getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/shared-links/me';
|
||||
@@ -283,10 +289,12 @@ class SharedLinksApi {
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] password:
|
||||
/// Link password
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] token:
|
||||
/// Access token
|
||||
Future<SharedLinkResponseDto?> getMySharedLink({ String? key, String? password, String? slug, String? token, }) async {
|
||||
final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
2
mobile/openapi/lib/api/stacks_api.dart
generated
2
mobile/openapi/lib/api/stacks_api.dart
generated
@@ -289,6 +289,7 @@ class StacksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] primaryAssetId:
|
||||
/// Filter by primary asset ID
|
||||
Future<Response> searchStacksWithHttpInfo({ String? primaryAssetId, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/stacks';
|
||||
@@ -325,6 +326,7 @@ class StacksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] primaryAssetId:
|
||||
/// Filter by primary asset ID
|
||||
Future<List<StackResponseDto>?> searchStacks({ String? primaryAssetId, }) async {
|
||||
final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
10
mobile/openapi/lib/api/users_admin_api.dart
generated
10
mobile/openapi/lib/api/users_admin_api.dart
generated
@@ -318,10 +318,13 @@ class UsersAdminApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Filter by trash status
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
Future<Response> getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/users/{id}/statistics'
|
||||
@@ -367,10 +370,13 @@ class UsersAdminApi {
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
/// Filter by favorite status
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
/// Filter by trash status
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by visibility
|
||||
Future<AssetStatsResponseDto?> getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
@@ -452,8 +458,10 @@ class UsersAdminApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
/// User ID filter
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
/// Include deleted users
|
||||
Future<Response> searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/users';
|
||||
@@ -493,8 +501,10 @@ class UsersAdminApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
/// User ID filter
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
/// Include deleted users
|
||||
Future<List<UserAdminResponseDto>?> searchUsersAdmin({ String? id, bool? withDeleted, }) async {
|
||||
final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
2
mobile/openapi/lib/api/users_api.dart
generated
2
mobile/openapi/lib/api/users_api.dart
generated
@@ -25,6 +25,7 @@ class UsersApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MultipartFile] file (required):
|
||||
/// Profile image file
|
||||
Future<Response> createProfileImageWithHttpInfo(MultipartFile file,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/users/profile-image';
|
||||
@@ -67,6 +68,7 @@ class UsersApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MultipartFile] file (required):
|
||||
/// Profile image file
|
||||
Future<CreateProfileImageResponseDto?> createProfileImage(MultipartFile file,) async {
|
||||
final response = await createProfileImageWithHttpInfo(file,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
|
||||
@@ -19,8 +19,10 @@ class ActivityCreateDto {
|
||||
required this.type,
|
||||
});
|
||||
|
||||
/// Album ID
|
||||
String albumId;
|
||||
|
||||
/// Asset ID (if activity is for an asset)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -29,6 +31,7 @@ class ActivityCreateDto {
|
||||
///
|
||||
String? assetId;
|
||||
|
||||
/// Comment text (required if type is comment)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -37,6 +40,7 @@ class ActivityCreateDto {
|
||||
///
|
||||
String? comment;
|
||||
|
||||
/// Activity type (like or comment)
|
||||
ReactionType type;
|
||||
|
||||
@override
|
||||
|
||||
@@ -21,14 +21,19 @@ class ActivityResponseDto {
|
||||
required this.user,
|
||||
});
|
||||
|
||||
/// Asset ID (if activity is for an asset)
|
||||
String? assetId;
|
||||
|
||||
/// Comment text (for comment activities)
|
||||
String? comment;
|
||||
|
||||
/// Creation date
|
||||
DateTime createdAt;
|
||||
|
||||
/// Activity ID
|
||||
String id;
|
||||
|
||||
/// Activity type
|
||||
ReactionType type;
|
||||
|
||||
UserResponseDto user;
|
||||
|
||||
@@ -17,8 +17,10 @@ class ActivityStatisticsResponseDto {
|
||||
required this.likes,
|
||||
});
|
||||
|
||||
/// Number of comments
|
||||
int comments;
|
||||
|
||||
/// Number of likes
|
||||
int likes;
|
||||
|
||||
@override
|
||||
|
||||
1
mobile/openapi/lib/model/add_users_dto.dart
generated
1
mobile/openapi/lib/model/add_users_dto.dart
generated
@@ -16,6 +16,7 @@ class AddUsersDto {
|
||||
this.albumUsers = const [],
|
||||
});
|
||||
|
||||
/// Album users to add
|
||||
List<AlbumUserAddDto> albumUsers;
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,6 +16,7 @@ class AdminOnboardingUpdateDto {
|
||||
required this.isOnboarded,
|
||||
});
|
||||
|
||||
/// Is admin onboarded
|
||||
bool isOnboarded;
|
||||
|
||||
@override
|
||||
|
||||
15
mobile/openapi/lib/model/album_response_dto.dart
generated
15
mobile/openapi/lib/model/album_response_dto.dart
generated
@@ -34,22 +34,28 @@ class AlbumResponseDto {
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Album name
|
||||
String albumName;
|
||||
|
||||
/// Thumbnail asset ID
|
||||
String? albumThumbnailAssetId;
|
||||
|
||||
List<AlbumUserResponseDto> albumUsers;
|
||||
|
||||
/// Number of assets
|
||||
int assetCount;
|
||||
|
||||
List<AssetResponseDto> assets;
|
||||
|
||||
List<ContributorCountResponseDto> contributorCounts;
|
||||
|
||||
/// Creation date
|
||||
DateTime createdAt;
|
||||
|
||||
/// Album description
|
||||
String description;
|
||||
|
||||
/// End date (latest asset)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -58,12 +64,16 @@ class AlbumResponseDto {
|
||||
///
|
||||
DateTime? endDate;
|
||||
|
||||
/// Has shared link
|
||||
bool hasSharedLink;
|
||||
|
||||
/// Album ID
|
||||
String id;
|
||||
|
||||
/// Activity feed enabled
|
||||
bool isActivityEnabled;
|
||||
|
||||
/// Last modified asset timestamp
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -72,6 +82,7 @@ class AlbumResponseDto {
|
||||
///
|
||||
DateTime? lastModifiedAssetTimestamp;
|
||||
|
||||
/// Asset sort order
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -82,10 +93,13 @@ class AlbumResponseDto {
|
||||
|
||||
UserResponseDto owner;
|
||||
|
||||
/// Owner user ID
|
||||
String ownerId;
|
||||
|
||||
/// Is shared album
|
||||
bool shared;
|
||||
|
||||
/// Start date (earliest asset)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -94,6 +108,7 @@ class AlbumResponseDto {
|
||||
///
|
||||
DateTime? startDate;
|
||||
|
||||
/// Last update date
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,10 +18,13 @@ class AlbumStatisticsResponseDto {
|
||||
required this.shared,
|
||||
});
|
||||
|
||||
/// Number of non-shared albums
|
||||
int notShared;
|
||||
|
||||
/// Number of owned albums
|
||||
int owned;
|
||||
|
||||
/// Number of shared albums
|
||||
int shared;
|
||||
|
||||
@override
|
||||
|
||||
2
mobile/openapi/lib/model/album_user_add_dto.dart
generated
2
mobile/openapi/lib/model/album_user_add_dto.dart
generated
@@ -17,8 +17,10 @@ class AlbumUserAddDto {
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
/// Album user role
|
||||
AlbumUserRole role;
|
||||
|
||||
/// User ID
|
||||
String userId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,8 +17,10 @@ class AlbumUserCreateDto {
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
/// Album user role
|
||||
AlbumUserRole role;
|
||||
|
||||
/// User ID
|
||||
String userId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class AlbumUserResponseDto {
|
||||
required this.user,
|
||||
});
|
||||
|
||||
/// Album user role
|
||||
AlbumUserRole role;
|
||||
|
||||
UserResponseDto user;
|
||||
|
||||
2
mobile/openapi/lib/model/album_user_role.dart
generated
2
mobile/openapi/lib/model/album_user_role.dart
generated
@@ -10,7 +10,7 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
/// Album user role
|
||||
class AlbumUserRole {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AlbumUserRole._(this.value);
|
||||
|
||||
@@ -17,8 +17,10 @@ class AlbumsAddAssetsDto {
|
||||
this.assetIds = const [],
|
||||
});
|
||||
|
||||
/// Album IDs
|
||||
List<String> albumIds;
|
||||
|
||||
/// Asset IDs
|
||||
List<String> assetIds;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class AlbumsAddAssetsResponseDto {
|
||||
required this.success,
|
||||
});
|
||||
|
||||
/// Error reason
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -25,6 +26,7 @@ class AlbumsAddAssetsResponseDto {
|
||||
///
|
||||
BulkIdErrorReason? error;
|
||||
|
||||
/// Operation success
|
||||
bool success;
|
||||
|
||||
@override
|
||||
|
||||
1
mobile/openapi/lib/model/albums_response.dart
generated
1
mobile/openapi/lib/model/albums_response.dart
generated
@@ -16,6 +16,7 @@ class AlbumsResponse {
|
||||
this.defaultAssetOrder = AssetOrder.desc,
|
||||
});
|
||||
|
||||
/// Default asset order for albums
|
||||
AssetOrder defaultAssetOrder;
|
||||
|
||||
@override
|
||||
|
||||
1
mobile/openapi/lib/model/albums_update.dart
generated
1
mobile/openapi/lib/model/albums_update.dart
generated
@@ -16,6 +16,7 @@ class AlbumsUpdate {
|
||||
this.defaultAssetOrder,
|
||||
});
|
||||
|
||||
/// Default asset order for albums
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
2
mobile/openapi/lib/model/api_key_create_dto.dart
generated
2
mobile/openapi/lib/model/api_key_create_dto.dart
generated
@@ -17,6 +17,7 @@ class APIKeyCreateDto {
|
||||
this.permissions = const [],
|
||||
});
|
||||
|
||||
/// API key name
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -25,6 +26,7 @@ class APIKeyCreateDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// List of permissions
|
||||
List<Permission> permissions;
|
||||
|
||||
@override
|
||||
|
||||
@@ -19,6 +19,7 @@ class APIKeyCreateResponseDto {
|
||||
|
||||
APIKeyResponseDto apiKey;
|
||||
|
||||
/// API key secret (only shown once)
|
||||
String secret;
|
||||
|
||||
@override
|
||||
|
||||
@@ -20,14 +20,19 @@ class APIKeyResponseDto {
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Creation date
|
||||
DateTime createdAt;
|
||||
|
||||
/// API key ID
|
||||
String id;
|
||||
|
||||
/// API key name
|
||||
String name;
|
||||
|
||||
/// List of permissions
|
||||
List<Permission> permissions;
|
||||
|
||||
/// Last update date
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
|
||||
2
mobile/openapi/lib/model/api_key_update_dto.dart
generated
2
mobile/openapi/lib/model/api_key_update_dto.dart
generated
@@ -17,6 +17,7 @@ class APIKeyUpdateDto {
|
||||
this.permissions = const [],
|
||||
});
|
||||
|
||||
/// API key name
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -25,6 +26,7 @@ class APIKeyUpdateDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// List of permissions
|
||||
List<Permission> permissions;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetBulkDeleteDto {
|
||||
this.ids = const [],
|
||||
});
|
||||
|
||||
/// Force delete even if in use
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -25,6 +26,7 @@ class AssetBulkDeleteDto {
|
||||
///
|
||||
bool? force;
|
||||
|
||||
/// IDs to process
|
||||
List<String> ids;
|
||||
|
||||
@override
|
||||
|
||||
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -26,6 +26,7 @@ class AssetBulkUpdateDto {
|
||||
this.visibility,
|
||||
});
|
||||
|
||||
/// Original date and time
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -34,6 +35,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
String? dateTimeOriginal;
|
||||
|
||||
/// Relative time offset in seconds
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -42,6 +44,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? dateTimeRelative;
|
||||
|
||||
/// Asset description
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -50,10 +53,13 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
String? description;
|
||||
|
||||
/// Duplicate asset ID
|
||||
String? duplicateId;
|
||||
|
||||
/// Asset IDs to update
|
||||
List<String> ids;
|
||||
|
||||
/// Mark as favorite
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -62,6 +68,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
bool? isFavorite;
|
||||
|
||||
/// Latitude coordinate
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -70,6 +77,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? latitude;
|
||||
|
||||
/// Longitude coordinate
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -78,6 +86,8 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
/// Rating
|
||||
///
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
@@ -88,6 +98,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? rating;
|
||||
|
||||
/// Time zone (IANA timezone)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -96,6 +107,7 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
String? timeZone;
|
||||
|
||||
/// Asset visibility
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
@@ -16,6 +16,7 @@ class AssetBulkUploadCheckDto {
|
||||
this.assets = const [],
|
||||
});
|
||||
|
||||
/// Assets to check
|
||||
List<AssetBulkUploadCheckItem> assets;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,9 +17,10 @@ class AssetBulkUploadCheckItem {
|
||||
required this.id,
|
||||
});
|
||||
|
||||
/// base64 or hex encoded sha1 hash
|
||||
/// Base64 or hex encoded SHA1 hash
|
||||
String checksum;
|
||||
|
||||
/// Asset ID
|
||||
String id;
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,6 +16,7 @@ class AssetBulkUploadCheckResponseDto {
|
||||
this.results = const [],
|
||||
});
|
||||
|
||||
/// Upload check results
|
||||
List<AssetBulkUploadCheckResult> results;
|
||||
|
||||
@override
|
||||
|
||||
@@ -20,8 +20,10 @@ class AssetBulkUploadCheckResult {
|
||||
this.reason,
|
||||
});
|
||||
|
||||
/// Upload action
|
||||
AssetBulkUploadCheckResultActionEnum action;
|
||||
|
||||
/// Existing asset ID if duplicate
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -30,8 +32,10 @@ class AssetBulkUploadCheckResult {
|
||||
///
|
||||
String? assetId;
|
||||
|
||||
/// Asset ID
|
||||
String id;
|
||||
|
||||
/// Whether existing asset is trashed
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -40,6 +44,7 @@ class AssetBulkUploadCheckResult {
|
||||
///
|
||||
bool? isTrashed;
|
||||
|
||||
/// Rejection reason if rejected
|
||||
AssetBulkUploadCheckResultReasonEnum? reason;
|
||||
|
||||
@override
|
||||
@@ -150,7 +155,7 @@ class AssetBulkUploadCheckResult {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Upload action
|
||||
class AssetBulkUploadCheckResultActionEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetBulkUploadCheckResultActionEnum._(this.value);
|
||||
@@ -224,7 +229,7 @@ class AssetBulkUploadCheckResultActionEnumTypeTransformer {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Rejection reason if rejected
|
||||
class AssetBulkUploadCheckResultReasonEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetBulkUploadCheckResultReasonEnum._(this.value);
|
||||
|
||||
7
mobile/openapi/lib/model/asset_copy_dto.dart
generated
7
mobile/openapi/lib/model/asset_copy_dto.dart
generated
@@ -22,18 +22,25 @@ class AssetCopyDto {
|
||||
required this.targetId,
|
||||
});
|
||||
|
||||
/// Copy album associations
|
||||
bool albums;
|
||||
|
||||
/// Copy favorite status
|
||||
bool favorite;
|
||||
|
||||
/// Copy shared links
|
||||
bool sharedLinks;
|
||||
|
||||
/// Copy sidecar file
|
||||
bool sidecar;
|
||||
|
||||
/// Source asset ID
|
||||
String sourceId;
|
||||
|
||||
/// Copy stack association
|
||||
bool stack;
|
||||
|
||||
/// Target asset ID
|
||||
String targetId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,8 +17,10 @@ class AssetDeltaSyncDto {
|
||||
this.userIds = const [],
|
||||
});
|
||||
|
||||
/// Sync assets updated after this date
|
||||
DateTime updatedAfter;
|
||||
|
||||
/// User IDs to sync
|
||||
List<String> userIds;
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,10 +18,13 @@ class AssetDeltaSyncResponseDto {
|
||||
this.upserted = const [],
|
||||
});
|
||||
|
||||
/// Deleted asset IDs
|
||||
List<String> deleted;
|
||||
|
||||
/// Whether full sync is needed
|
||||
bool needsFullSync;
|
||||
|
||||
/// Upserted assets
|
||||
List<AssetResponseDto> upserted;
|
||||
|
||||
@override
|
||||
|
||||
2
mobile/openapi/lib/model/asset_edit_action.dart
generated
2
mobile/openapi/lib/model/asset_edit_action.dart
generated
@@ -10,7 +10,7 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
/// Type of edit action to perform
|
||||
class AssetEditAction {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetEditAction._(this.value);
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetEditActionCrop {
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
/// Type of edit action to perform
|
||||
AssetEditAction action;
|
||||
|
||||
CropParameters parameters;
|
||||
|
||||
@@ -16,7 +16,7 @@ class AssetEditActionListDto {
|
||||
this.edits = const [],
|
||||
});
|
||||
|
||||
/// list of edits
|
||||
/// List of edit actions to apply (crop, rotate, or mirror)
|
||||
List<AssetEditActionListDtoEditsInner> edits;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetEditActionListDtoEditsInner {
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
/// Type of edit action to perform
|
||||
AssetEditAction action;
|
||||
|
||||
MirrorParameters parameters;
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetEditActionMirror {
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
/// Type of edit action to perform
|
||||
AssetEditAction action;
|
||||
|
||||
MirrorParameters parameters;
|
||||
|
||||
@@ -17,6 +17,7 @@ class AssetEditActionRotate {
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
/// Type of edit action to perform
|
||||
AssetEditAction action;
|
||||
|
||||
RotateParameters parameters;
|
||||
|
||||
3
mobile/openapi/lib/model/asset_edits_dto.dart
generated
3
mobile/openapi/lib/model/asset_edits_dto.dart
generated
@@ -17,9 +17,10 @@ class AssetEditsDto {
|
||||
this.edits = const [],
|
||||
});
|
||||
|
||||
/// Asset ID to apply edits to
|
||||
String assetId;
|
||||
|
||||
/// list of edits
|
||||
/// List of edit actions to apply (crop, rotate, or mirror)
|
||||
List<AssetEditActionListDtoEditsInner> edits;
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,20 +23,28 @@ class AssetFaceCreateDto {
|
||||
required this.y,
|
||||
});
|
||||
|
||||
/// Asset ID
|
||||
String assetId;
|
||||
|
||||
/// Face bounding box height
|
||||
int height;
|
||||
|
||||
/// Image height in pixels
|
||||
int imageHeight;
|
||||
|
||||
/// Image width in pixels
|
||||
int imageWidth;
|
||||
|
||||
/// Person ID
|
||||
String personId;
|
||||
|
||||
/// Face bounding box width
|
||||
int width;
|
||||
|
||||
/// Face bounding box X coordinate
|
||||
int x;
|
||||
|
||||
/// Face bounding box Y coordinate
|
||||
int y;
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,6 +16,7 @@ class AssetFaceDeleteDto {
|
||||
required this.force,
|
||||
});
|
||||
|
||||
/// Force delete even if person has other faces
|
||||
bool force;
|
||||
|
||||
@override
|
||||
|
||||
@@ -24,22 +24,31 @@ class AssetFaceResponseDto {
|
||||
this.sourceType,
|
||||
});
|
||||
|
||||
/// Bounding box X1 coordinate
|
||||
int boundingBoxX1;
|
||||
|
||||
/// Bounding box X2 coordinate
|
||||
int boundingBoxX2;
|
||||
|
||||
/// Bounding box Y1 coordinate
|
||||
int boundingBoxY1;
|
||||
|
||||
/// Bounding box Y2 coordinate
|
||||
int boundingBoxY2;
|
||||
|
||||
/// Face ID
|
||||
String id;
|
||||
|
||||
/// Image height in pixels
|
||||
int imageHeight;
|
||||
|
||||
/// Image width in pixels
|
||||
int imageWidth;
|
||||
|
||||
/// Person associated with face
|
||||
PersonResponseDto? person;
|
||||
|
||||
/// Face detection source type
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
@@ -16,6 +16,7 @@ class AssetFaceUpdateDto {
|
||||
this.data = const [],
|
||||
});
|
||||
|
||||
/// Face update items
|
||||
List<AssetFaceUpdateItem> data;
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,8 +17,10 @@ class AssetFaceUpdateItem {
|
||||
required this.personId,
|
||||
});
|
||||
|
||||
/// Asset ID
|
||||
String assetId;
|
||||
|
||||
/// Person ID
|
||||
String personId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,20 +23,28 @@ class AssetFaceWithoutPersonResponseDto {
|
||||
this.sourceType,
|
||||
});
|
||||
|
||||
/// Bounding box X1 coordinate
|
||||
int boundingBoxX1;
|
||||
|
||||
/// Bounding box X2 coordinate
|
||||
int boundingBoxX2;
|
||||
|
||||
/// Bounding box Y1 coordinate
|
||||
int boundingBoxY1;
|
||||
|
||||
/// Bounding box Y2 coordinate
|
||||
int boundingBoxY2;
|
||||
|
||||
/// Face ID
|
||||
String id;
|
||||
|
||||
/// Image height in pixels
|
||||
int imageHeight;
|
||||
|
||||
/// Image width in pixels
|
||||
int imageWidth;
|
||||
|
||||
/// Face detection source type
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
@@ -19,6 +19,7 @@ class AssetFullSyncDto {
|
||||
this.userId,
|
||||
});
|
||||
|
||||
/// Last asset ID (pagination)
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -27,11 +28,15 @@ class AssetFullSyncDto {
|
||||
///
|
||||
String? lastId;
|
||||
|
||||
/// Maximum number of assets to return
|
||||
///
|
||||
/// Minimum value: 1
|
||||
int limit;
|
||||
|
||||
/// Sync assets updated until this date
|
||||
DateTime updatedUntil;
|
||||
|
||||
/// Filter by user ID
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
1
mobile/openapi/lib/model/asset_ids_dto.dart
generated
1
mobile/openapi/lib/model/asset_ids_dto.dart
generated
@@ -16,6 +16,7 @@ class AssetIdsDto {
|
||||
this.assetIds = const [],
|
||||
});
|
||||
|
||||
/// Asset IDs
|
||||
List<String> assetIds;
|
||||
|
||||
@override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user