merge main
This commit is contained in:
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.32.8"
|
||||
"flutter": "3.35.3"
|
||||
}
|
||||
Vendored
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.32.8",
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.3",
|
||||
"dart.lineLength": 120,
|
||||
"[dart]": {
|
||||
"editor.rulers": [120],
|
||||
"editor.rulers": [120]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
|
||||
@@ -44,9 +44,10 @@ analyzer:
|
||||
- lib/**/*.g.dart
|
||||
- lib/**/*.drift.dart
|
||||
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
||||
# TODO: Re-enable after upgrading custom_lint
|
||||
# plugins:
|
||||
# - custom_lint
|
||||
|
||||
errors:
|
||||
unawaited_futures: warning
|
||||
|
||||
@@ -138,6 +139,13 @@ custom_lint:
|
||||
|
||||
dart_code_metrics:
|
||||
rules:
|
||||
- banned-usage:
|
||||
entries:
|
||||
- name: debugPrint
|
||||
description: Use dPrint instead of debugPrint for proper tree-shaking in release builds.
|
||||
exclude-paths:
|
||||
- 'lib/utils/debug_print.dart'
|
||||
severity: perf
|
||||
- avoid-unused-parameters
|
||||
- prefer-const-border-radius
|
||||
- prefer-declaring-const-constructor
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.os.Build
|
||||
import android.os.ext.SdkExtensions
|
||||
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
||||
import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
||||
import app.alextran.immich.connectivity.ConnectivityApi
|
||||
import app.alextran.immich.connectivity.ConnectivityApiImpl
|
||||
import app.alextran.immich.images.ThumbnailApi
|
||||
import app.alextran.immich.images.ThumbnailsImpl
|
||||
import app.alextran.immich.sync.NativeSyncApi
|
||||
@@ -34,6 +36,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||
ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx))
|
||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -111,6 +111,7 @@ interface BackgroundWorkerFgHostApi {
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface BackgroundWorkerBgHostApi {
|
||||
fun onInitialized()
|
||||
fun showNotification(title: String, content: String)
|
||||
fun close()
|
||||
|
||||
companion object {
|
||||
@@ -138,6 +139,25 @@ interface BackgroundWorkerBgHostApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val titleArg = args[0] as String
|
||||
val contentArg = args[1] as String
|
||||
val wrapped: List<Any?> = try {
|
||||
api.showNotification(titleArg, contentArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
|
||||
+84
-4
@@ -1,18 +1,27 @@
|
||||
package app.alextran.immich.background
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.alextran.immich.MainActivity
|
||||
import app.alextran.immich.R
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "BackgroundWorker"
|
||||
|
||||
@@ -40,15 +49,30 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
/// Flag to track whether the background task has completed to prevent duplicate completions
|
||||
private var isComplete = false
|
||||
|
||||
init {
|
||||
if (!loader.initialized()) {
|
||||
loader.startInitialization(ctx)
|
||||
}
|
||||
private val notificationManager =
|
||||
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private var foregroundFuture: ListenableFuture<Void>? = null
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_CHANNEL_ID = "immich::background_worker::notif"
|
||||
private const val NOTIFICATION_ID = 100
|
||||
}
|
||||
|
||||
override fun startWork(): ListenableFuture<Result> {
|
||||
Log.i(TAG, "Starting background upload worker")
|
||||
|
||||
if (!loader.initialized()) {
|
||||
loader.startInitialization(ctx)
|
||||
}
|
||||
|
||||
val notificationChannel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
|
||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||
engine = FlutterEngine(ctx)
|
||||
|
||||
@@ -82,6 +106,34 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||
}
|
||||
|
||||
// TODO: Move this to a separate NotificationManager class
|
||||
override fun showNotification(title: String, content: String) {
|
||||
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.notification_icon)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setOngoing(true)
|
||||
.setTicker(title)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
|
||||
if (isIgnoringBatteryOptimizations()) {
|
||||
foregroundFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
setForegroundAsync(
|
||||
ForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
)
|
||||
} else {
|
||||
setForegroundAsync(ForegroundInfo(NOTIFICATION_ID, notification))
|
||||
}
|
||||
} else {
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (isComplete) {
|
||||
return
|
||||
@@ -95,6 +147,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
}
|
||||
}
|
||||
|
||||
waitForForegroundPromotion()
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
complete(Result.failure())
|
||||
}, 5000)
|
||||
@@ -135,6 +189,32 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
engine?.destroy()
|
||||
engine = null
|
||||
flutterApi = null
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
waitForForegroundPromotion()
|
||||
completionHandler.set(success)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the app is ignoring battery optimizations
|
||||
*/
|
||||
private fun isIgnoringBatteryOptimizations(): Boolean {
|
||||
val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls to setForegroundAsync() that do not complete before completion of a ListenableWorker will signal an IllegalStateException
|
||||
* https://android-review.googlesource.com/c/platform/frameworks/support/+/1262743
|
||||
* Wait for a short period of time for the foreground promotion to complete before completing the worker
|
||||
*/
|
||||
private fun waitForForegroundPromotion() {
|
||||
val foregroundFuture = this.foregroundFuture
|
||||
if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) {
|
||||
try {
|
||||
foregroundFuture.get(500, TimeUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
// ignored, there is nothing to be done
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package app.alextran.immich.connectivity
|
||||
|
||||
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 ConnectivityPigeonUtils {
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
enum class NetworkCapability(val raw: Int) {
|
||||
CELLULAR(0),
|
||||
WIFI(1),
|
||||
VPN(2),
|
||||
UNMETERED(3);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): NetworkCapability? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
private open class ConnectivityPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
NetworkCapability.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is NetworkCapability -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface ConnectivityApi {
|
||||
fun getCapabilities(): List<NetworkCapability>
|
||||
|
||||
companion object {
|
||||
/** The codec used by ConnectivityApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
ConnectivityPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getCapabilities())
|
||||
} catch (exception: Throwable) {
|
||||
ConnectivityPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package app.alextran.immich.connectivity
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
|
||||
class ConnectivityApiImpl(context: Context) : ConnectivityApi {
|
||||
private val connectivityManager =
|
||||
context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
private val wifiManager =
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
override fun getCapabilities(): List<NetworkCapability> {
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
?: return emptyList()
|
||||
|
||||
val hasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
val hasCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasVpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||
val isUnmetered = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
|
||||
return buildList {
|
||||
if (hasWifi) add(NetworkCapability.WIFI)
|
||||
if (hasCellular) add(NetworkCapability.CELLULAR)
|
||||
if (hasVpn) {
|
||||
add(NetworkCapability.VPN)
|
||||
if (!hasWifi && !hasCellular) {
|
||||
if (wifiManager.isWifiEnabled) add(NetworkCapability.WIFI)
|
||||
// If VPN is active, but neither WIFI nor CELLULAR is reported as active,
|
||||
// assume CELLULAR if WIFI is not enabled
|
||||
else add(NetworkCapability.CELLULAR)
|
||||
}
|
||||
}
|
||||
if (isUnmetered) add(NetworkCapability.UNMETERED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3013,
|
||||
"android.injected.version.name" => "1.141.1",
|
||||
"android.injected.version.code" => 3015,
|
||||
"android.injected.version.name" => "1.142.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -253,7 +253,7 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
@@ -97,6 +99,8 @@
|
||||
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -243,6 +247,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||
@@ -271,6 +276,15 @@
|
||||
path = Background;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B25D37792E72CA15008B6CA7 /* Connectivity */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */,
|
||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */,
|
||||
);
|
||||
path = Connectivity;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -558,7 +572,9 @@
|
||||
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
|
||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
|
||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
|
||||
@@ -689,7 +705,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -833,7 +849,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -863,7 +879,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -897,7 +913,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -940,7 +956,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -980,7 +996,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1019,7 +1035,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1063,7 +1079,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1104,7 +1120,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 219;
|
||||
CURRENT_PROJECT_VERSION = 224;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -114,6 +114,7 @@ class BackgroundWorkerFgHostApiSetup {
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol BackgroundWorkerBgHostApi {
|
||||
func onInitialized() throws
|
||||
func showNotification(title: String, content: String) throws
|
||||
func close() throws
|
||||
}
|
||||
|
||||
@@ -136,6 +137,22 @@ class BackgroundWorkerBgHostApiSetup {
|
||||
} else {
|
||||
onInitializedChannel.setMessageHandler(nil)
|
||||
}
|
||||
let showNotificationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
showNotificationChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let titleArg = args[0] as! String
|
||||
let contentArg = args[1] as! String
|
||||
do {
|
||||
try api.showNotification(title: titleArg, content: contentArg)
|
||||
reply(wrapResult(nil))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showNotificationChannel.setMessageHandler(nil)
|
||||
}
|
||||
let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
closeChannel.setMessageHandler { _, reply in
|
||||
|
||||
@@ -119,6 +119,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
})
|
||||
}
|
||||
|
||||
func showNotification(title: String, content: String) throws {
|
||||
// No-op on iOS for the time being
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the currently running background task, either due to timeout or external request.
|
||||
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||
@@ -129,7 +133,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
return
|
||||
}
|
||||
|
||||
isComplete = true
|
||||
flutterApi?.cancel { result in
|
||||
self.complete(success: false)
|
||||
}
|
||||
@@ -170,6 +173,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
|
||||
isComplete = true
|
||||
engine.destroyContext()
|
||||
flutterApi = nil
|
||||
completionHandler(success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
||||
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
||||
private static let taskSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
public static func registerBackgroundWorkers() {
|
||||
BGTaskScheduler.shared.register(
|
||||
@@ -59,12 +60,18 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||
scheduleRefreshWorker()
|
||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||
// If another task is running, cede the background time back to the OS
|
||||
if taskSemaphore.wait(timeout: .now()) == .success {
|
||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||
} else {
|
||||
task.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||
scheduleProcessingWorker()
|
||||
taskSemaphore.wait()
|
||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
||||
}
|
||||
@@ -80,6 +87,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
* - maxSeconds: Optional timeout for the operation in seconds
|
||||
*/
|
||||
private static func runBackgroundWorker(task: BGTask, taskType: BackgroundTaskType, maxSeconds: Int?) {
|
||||
defer { taskSemaphore.signal() }
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var isSuccess = true
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), 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?
|
||||
}
|
||||
|
||||
|
||||
enum NetworkCapability: Int {
|
||||
case cellular = 0
|
||||
case wifi = 1
|
||||
case vpn = 2
|
||||
case unmetered = 3
|
||||
}
|
||||
|
||||
private class ConnectivityPigeonCodecReader: FlutterStandardReader {
|
||||
override func readValue(ofType type: UInt8) -> Any? {
|
||||
switch type {
|
||||
case 129:
|
||||
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
||||
if let enumResultAsInt = enumResultAsInt {
|
||||
return NetworkCapability(rawValue: enumResultAsInt)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConnectivityPigeonCodecWriter: FlutterStandardWriter {
|
||||
override func writeValue(_ value: Any) {
|
||||
if let value = value as? NetworkCapability {
|
||||
super.writeByte(129)
|
||||
super.writeValue(value.rawValue)
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return ConnectivityPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return ConnectivityPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol ConnectivityApi {
|
||||
func getCapabilities() throws -> [NetworkCapability]
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class ConnectivityApiSetup {
|
||||
static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared }
|
||||
/// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
#if os(iOS)
|
||||
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
|
||||
#else
|
||||
let taskQueue: FlutterTaskQueue? = nil
|
||||
#endif
|
||||
let getCapabilitiesChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||
if let api = api {
|
||||
getCapabilitiesChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
let result = try api.getCapabilities()
|
||||
reply(wrapResult(result))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getCapabilitiesChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
class ConnectivityApiImpl: ConnectivityApi {
|
||||
func getCapabilities() throws -> [NetworkCapability] {
|
||||
[]
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.140.0</string>
|
||||
<string>1.142.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -107,7 +107,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>219</string>
|
||||
<string>224</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -22,7 +22,7 @@ platform :ios do
|
||||
path: "./Runner.xcodeproj",
|
||||
)
|
||||
increment_version_number(
|
||||
version_number: "1.141.1"
|
||||
version_number: "1.142.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -45,3 +45,5 @@ const List<(String, String)> kWidgetNames = [
|
||||
|
||||
const double kUploadStatusFailed = -1.0;
|
||||
const double kUploadStatusCanceled = -2.0;
|
||||
|
||||
const int kMinMonthsToEnableScrubberSnap = 12;
|
||||
|
||||
@@ -76,7 +76,10 @@ enum StoreKey<T> {
|
||||
betaTimeline<bool>._(1002),
|
||||
enableBackup<bool>._(1003),
|
||||
useWifiForUploadVideos<bool>._(1004),
|
||||
useWifiForUploadPhotos<bool>._(1005);
|
||||
useWifiForUploadPhotos<bool>._(1005),
|
||||
needBetaMigration<bool>._(1006),
|
||||
// TODO: Remove this after patching open-api
|
||||
shouldResetSync<bool>._(1007);
|
||||
|
||||
const StoreKey._(this.id);
|
||||
final int id;
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary("primary"),
|
||||
pink("pink"),
|
||||
red("red"),
|
||||
yellow("yellow"),
|
||||
blue("blue"),
|
||||
green("green"),
|
||||
purple("purple"),
|
||||
orange("orange"),
|
||||
gray("gray"),
|
||||
amber("amber");
|
||||
|
||||
final String value;
|
||||
const AvatarColor(this.value);
|
||||
|
||||
Color toColor({bool isDarkTheme = false}) => switch (this) {
|
||||
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Rename to User once Isar is removed
|
||||
class UserDto {
|
||||
@@ -9,7 +38,7 @@ class UserDto {
|
||||
final String email;
|
||||
final String name;
|
||||
final bool isAdmin;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
final AvatarColor avatarColor;
|
||||
|
||||
@@ -31,8 +60,8 @@ class UserDto {
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.isAdmin,
|
||||
required this.updatedAt,
|
||||
this.isAdmin = false,
|
||||
this.updatedAt,
|
||||
required this.profileChangedAt,
|
||||
this.avatarColor = AvatarColor.primary,
|
||||
this.memoryEnabled = true,
|
||||
@@ -99,7 +128,8 @@ profileChangedAt: $profileChangedAt
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.updatedAt.isAtSameMomentAs(updatedAt) &&
|
||||
((updatedAt == null && other.updatedAt == null) ||
|
||||
(updatedAt != null && other.updatedAt != null && other.updatedAt!.isAtSameMomentAs(updatedAt!))) &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.name == name &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:ui';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
enum UserMetadataKey {
|
||||
// do not change this order!
|
||||
@@ -7,36 +7,6 @@ enum UserMetadataKey {
|
||||
license,
|
||||
}
|
||||
|
||||
enum AvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary("primary"),
|
||||
pink("pink"),
|
||||
red("red"),
|
||||
yellow("yellow"),
|
||||
blue("blue"),
|
||||
green("green"),
|
||||
purple("purple"),
|
||||
orange("orange"),
|
||||
gray("gray"),
|
||||
amber("amber");
|
||||
|
||||
final String value;
|
||||
const AvatarColor(this.value);
|
||||
|
||||
Color toColor({bool isDarkTheme = false}) => switch (this) {
|
||||
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
|
||||
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
|
||||
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
|
||||
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
|
||||
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
|
||||
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
|
||||
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
|
||||
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
|
||||
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
|
||||
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
|
||||
};
|
||||
}
|
||||
|
||||
class Onboarding {
|
||||
final bool isOnboarded;
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
class AssetService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final Platform _platform;
|
||||
|
||||
const AssetService({
|
||||
required RemoteAssetRepository remoteAssetRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
}) : _remoteAssetRepository = remoteAssetRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_platform = const LocalPlatform();
|
||||
_localAssetRepository = localAssetRepository;
|
||||
|
||||
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||
@@ -27,6 +26,14 @@ class AssetService {
|
||||
return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id);
|
||||
}
|
||||
|
||||
Future<List<LocalAsset?>> getLocalAssetsByChecksum(String checksum) {
|
||||
return _localAssetRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||
return _remoteAssetRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAsset(String id) {
|
||||
return _remoteAssetRepository.get(id);
|
||||
}
|
||||
@@ -62,7 +69,7 @@ class AssetService {
|
||||
width = exif?.width ?? asset.width?.toDouble();
|
||||
height = exif?.height ?? asset.height?.toDouble();
|
||||
} else if (asset is LocalAsset) {
|
||||
isFlipped = _platform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
|
||||
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
|
||||
width = asset.width?.toDouble();
|
||||
height = asset.height?.toDouble();
|
||||
} else {
|
||||
@@ -89,4 +96,8 @@ class AssetService {
|
||||
Future<int> getLocalHashedCount() {
|
||||
return _localAssetRepository.getHashedCount();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
@@ -14,16 +20,20 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
class BackgroundWorkerFgService {
|
||||
final BackgroundWorkerFgHostApi _foregroundHostApi;
|
||||
@@ -42,8 +52,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
final Drift _drift;
|
||||
final DriftLogger _driftLogger;
|
||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||
late final IsolateLockManager _lockManager;
|
||||
final CancellationToken _cancellationToken = CancellationToken();
|
||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
@@ -59,7 +69,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
@@ -67,41 +76,37 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
await loadTranslations();
|
||||
HttpSSLOptions.apply(applyNative: false);
|
||||
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
||||
|
||||
// Initialize the file downloader
|
||||
await FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
);
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
await FileDownloader().trackTasks();
|
||||
await Future.wait([
|
||||
loadTranslations(),
|
||||
workerManager.init(dynamicSpawning: true),
|
||||
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
||||
// Initialize the file downloader
|
||||
FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
),
|
||||
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
||||
FileDownloader().trackTasks(),
|
||||
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
||||
]);
|
||||
|
||||
configureFileDownloaderNotifications();
|
||||
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
||||
|
||||
// Notify the host that the background upload service has been initialized and is ready to use
|
||||
debugPrint("Acquiring background worker lock");
|
||||
if (await _lockManager.acquireLock().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
_lockManager.cancel();
|
||||
return false;
|
||||
},
|
||||
)) {
|
||||
_logger.info("Acquired background worker lock");
|
||||
await _backgroundHostApi.onInitialized();
|
||||
return;
|
||||
if (Platform.isAndroid) {
|
||||
await _backgroundHostApi.showNotification(
|
||||
IntlKeys.uploading_media.t(),
|
||||
IntlKeys.backup_background_service_in_progress_notification.t(),
|
||||
);
|
||||
}
|
||||
|
||||
_logger.warning("Failed to acquire background worker lock");
|
||||
await _cleanup();
|
||||
await _backgroundHostApi.close();
|
||||
// Notify the host that the background worker service has been initialized and is ready to use
|
||||
_backgroundHostApi.onInitialized();
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Failed to initialize background worker", error, stack);
|
||||
await _backgroundHostApi.close();
|
||||
@@ -115,7 +120,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
||||
await _handleBackup(processBulk: false);
|
||||
await _handleBackup();
|
||||
|
||||
sw.stop();
|
||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
||||
@@ -157,7 +162,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
try {
|
||||
await _cleanup();
|
||||
} catch (error, stack) {
|
||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
||||
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,70 +172,96 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
|
||||
try {
|
||||
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
|
||||
_isCleanedUp = true;
|
||||
_logger.info("Cleaning up background worker");
|
||||
await _ref.read(backgroundSyncProvider).cancel();
|
||||
await _ref.read(backgroundSyncProvider).cancelLocal();
|
||||
if (_isar.isOpen) {
|
||||
await _isar.close();
|
||||
}
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
_ref.dispose();
|
||||
_lockManager.releaseLock();
|
||||
|
||||
_cancellationToken.cancel();
|
||||
_logger.info("Cleaning up background worker");
|
||||
final cleanupFutures = [
|
||||
workerManager.dispose().catchError((_) async {
|
||||
// Discard any errors on the dispose call
|
||||
return;
|
||||
}),
|
||||
LogService.I.dispose(),
|
||||
Store.dispose(),
|
||||
_drift.close(),
|
||||
_driftLogger.close(),
|
||||
backgroundSyncManager.cancel(),
|
||||
backgroundSyncManager.cancelLocal(),
|
||||
];
|
||||
|
||||
if (_isar.isOpen) {
|
||||
cleanupFutures.add(_isar.close());
|
||||
}
|
||||
await Future.wait(cleanupFutures);
|
||||
_logger.info("Background worker resources cleaned up");
|
||||
} catch (error, stack) {
|
||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
||||
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||
if (!_isBackupEnabled) {
|
||||
return;
|
||||
}
|
||||
Future<void> _handleBackup() async {
|
||||
await runZonedGuarded(
|
||||
() async {
|
||||
if (!_isBackupEnabled || _isCleanedUp) {
|
||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
||||
return;
|
||||
}
|
||||
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
||||
|
||||
if (processBulk) {
|
||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
|
||||
return;
|
||||
}
|
||||
|
||||
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
|
||||
if (activeTask.isNotEmpty) {
|
||||
await _ref.read(uploadServiceProvider).resumeBackup();
|
||||
} else {
|
||||
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
|
||||
}
|
||||
_logger.info("[_handleBackup 4] Resume backup from background");
|
||||
if (Platform.isIOS) {
|
||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
|
||||
final canPing = await _ref.read(serverInfoServiceProvider).ping();
|
||||
if (!canPing) {
|
||||
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
|
||||
return;
|
||||
}
|
||||
|
||||
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
|
||||
|
||||
return _ref
|
||||
.read(uploadServiceProvider)
|
||||
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
|
||||
},
|
||||
(error, stack) {
|
||||
dPrint(() => "Error in backup zone $error, $stack");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||
final futures = <Future<void>>[];
|
||||
await _ref.read(backgroundSyncProvider).syncLocal();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
await _ref.read(backgroundSyncProvider).syncRemote();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||
if (hashTimeout != null) {
|
||||
hashFuture = hashFuture.timeout(
|
||||
hashTimeout,
|
||||
onTimeout: () {
|
||||
// Consume cancellation errors as we want to continue processing
|
||||
},
|
||||
);
|
||||
}
|
||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||
if (hashTimeout != null) {
|
||||
hashFuture = hashFuture.timeout(
|
||||
hashTimeout,
|
||||
onTimeout: () {
|
||||
// Consume cancellation errors as we want to continue processing
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return hashFuture;
|
||||
});
|
||||
|
||||
futures.add(localSyncFuture);
|
||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
||||
|
||||
await Future.wait(futures);
|
||||
await hashFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +273,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final (isar, drift, logDB) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
|
||||
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
|
||||
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
|
||||
}
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
class LocalSyncService {
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final Platform _platform;
|
||||
final Logger _log = Logger("DeviceSyncService");
|
||||
|
||||
LocalSyncService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
Platform? platform,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_nativeSyncApi = nativeSyncApi,
|
||||
_platform = platform ?? const LocalPlatform();
|
||||
LocalSyncService({required DriftLocalAlbumRepository localAlbumRepository, required NativeSyncApi nativeSyncApi})
|
||||
: _localAlbumRepository = localAlbumRepository,
|
||||
_nativeSyncApi = nativeSyncApi;
|
||||
|
||||
Future<void> sync({bool full = false}) async {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
@@ -53,14 +48,14 @@ class LocalSyncService {
|
||||
final dbAlbums = await _localAlbumRepository.getAll();
|
||||
// On Android, we need to sync all albums since it is not possible to
|
||||
// detect album deletions from the native side
|
||||
if (_platform.isAndroid) {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
for (final album in dbAlbums) {
|
||||
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
|
||||
await _localAlbumRepository.syncDeletes(album.id, deviceIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (_platform.isIOS) {
|
||||
if (CurrentPlatform.isIOS) {
|
||||
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
|
||||
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
|
||||
// remove the albums from the local database from the previous sync
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Service responsible for handling application logging.
|
||||
@@ -66,13 +66,12 @@ class LogService {
|
||||
}
|
||||
|
||||
void _handleLogRecord(LogRecord r) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
|
||||
'${r.error == null ? '' : '\nError: ${r.error}'}'
|
||||
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
|
||||
);
|
||||
}
|
||||
dPrint(
|
||||
() =>
|
||||
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
|
||||
'${r.error == null ? '' : '\nError: ${r.error}'}'
|
||||
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
|
||||
);
|
||||
|
||||
final record = LogMessage(
|
||||
message: r.message,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class DriftPartnerService {
|
||||
final DriftPartnerRepository _driftPartnerRepository;
|
||||
@@ -30,7 +30,7 @@ class DriftPartnerService {
|
||||
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
|
||||
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
|
||||
if (partner == null) {
|
||||
debugPrint("Partner not found: $partnerId for user: $userId");
|
||||
dPrint(() => "Partner not found: $partnerId for user: $userId");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class StoreService {
|
||||
|
||||
/// In-memory cache. Keys are [StoreKey.id]
|
||||
final Map<int, Object?> _cache = {};
|
||||
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
|
||||
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
|
||||
|
||||
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
|
||||
|
||||
@@ -24,15 +24,17 @@ class StoreService {
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
|
||||
_instance ??= await create(storeRepository: storeRepository);
|
||||
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
|
||||
static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
final instance = StoreService._(isarStoreRepository: storeRepository);
|
||||
await instance.populateCache();
|
||||
instance._storeUpdateSubscription = instance._listenForChange();
|
||||
if (listenUpdates) {
|
||||
instance._storeUpdateSubscription = instance._listenForChange();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -50,8 +52,8 @@ class StoreService {
|
||||
});
|
||||
|
||||
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
||||
void dispose() async {
|
||||
await _storeUpdateSubscription.cancel();
|
||||
Future<void> dispose() async {
|
||||
await _storeUpdateSubscription?.cancel();
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ class StoreService {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true;
|
||||
}
|
||||
|
||||
class StoreKeyNotFoundException implements Exception {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
final syncLinkedAlbumServiceProvider = Provider(
|
||||
(ref) => SyncLinkedAlbumService(
|
||||
@@ -19,7 +20,9 @@ class SyncLinkedAlbumService {
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
|
||||
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
final _log = Logger("SyncLinkedAlbumService");
|
||||
|
||||
Future<void> syncLinkedAlbums(String userId) async {
|
||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
@@ -48,8 +51,12 @@ class SyncLinkedAlbumService {
|
||||
}
|
||||
|
||||
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
try {
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Error managing linked albums", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +100,7 @@ class SyncLinkedAlbumService {
|
||||
|
||||
/// Creates a new remote album and links it to the local album
|
||||
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
|
||||
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
|
||||
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
|
||||
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
|
||||
await _remoteAlbumRepository.create(newRemoteAlbum, []);
|
||||
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
|
||||
|
||||
@@ -23,54 +23,18 @@ class SyncStreamService {
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
Future<void> sync() {
|
||||
Future<void> sync() async {
|
||||
_logger.info("Remote sync request for user");
|
||||
// Start the sync stream and handle events
|
||||
return _syncApiRepository.streamChanges(_handleEvents);
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
|
||||
if (batchData.isEmpty) return;
|
||||
|
||||
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
|
||||
|
||||
final List<SyncAssetV1> assets = [];
|
||||
final List<SyncAssetExifV1> exifs = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
final exifData = payload['exif'];
|
||||
|
||||
if (assetData == null || exifData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final asset = SyncAssetV1.fromJson(assetData);
|
||||
final exif = SyncAssetExifV1.fromJson(exifData);
|
||||
|
||||
if (asset != null && exif != null) {
|
||||
assets.add(asset);
|
||||
exifs.add(exif);
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty && exifs.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
|
||||
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
|
||||
_logger.info('Successfully processed ${assets.length} assets in batch');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
|
||||
bool shouldReset = false;
|
||||
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
||||
if (shouldReset) {
|
||||
_logger.info("Resetting sync state as requested by server");
|
||||
await _syncApiRepository.streamChanges(_handleEvents);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
|
||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
|
||||
List<SyncEvent> items = [];
|
||||
for (final event in events) {
|
||||
if (isCancelled) {
|
||||
@@ -83,6 +47,10 @@ class SyncStreamService {
|
||||
await _processBatch(items);
|
||||
}
|
||||
|
||||
if (event.type == SyncEntityType.syncResetV1) {
|
||||
reset();
|
||||
}
|
||||
|
||||
items.add(event);
|
||||
}
|
||||
|
||||
@@ -103,6 +71,8 @@ class SyncStreamService {
|
||||
Future<void> _handleSyncData(SyncEntityType type, Iterable<Object> data) async {
|
||||
_logger.fine("Processing sync data for $type of length ${data.length}");
|
||||
switch (type) {
|
||||
case SyncEntityType.authUserV1:
|
||||
return _syncStreamRepository.updateAuthUsersV1(data.cast());
|
||||
case SyncEntityType.userV1:
|
||||
return _syncStreamRepository.updateUsersV1(data.cast());
|
||||
case SyncEntityType.userDeleteV1:
|
||||
@@ -159,6 +129,12 @@ class SyncStreamService {
|
||||
// to acknowledge that the client has processed all the backfill events
|
||||
case SyncEntityType.syncAckV1:
|
||||
return;
|
||||
// No-op. SyncCompleteV1 is used to signal the completion of the sync process
|
||||
case SyncEntityType.syncCompleteV1:
|
||||
return;
|
||||
// Request to reset the client state. Clear everything related to remote entities
|
||||
case SyncEntityType.syncResetV1:
|
||||
return _syncStreamRepository.reset();
|
||||
case SyncEntityType.memoryV1:
|
||||
return _syncStreamRepository.updateMemoriesV1(data.cast());
|
||||
case SyncEntityType.memoryDeleteV1:
|
||||
@@ -193,4 +169,45 @@ class SyncStreamService {
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
|
||||
if (batchData.isEmpty) return;
|
||||
|
||||
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
|
||||
|
||||
final List<SyncAssetV1> assets = [];
|
||||
final List<SyncAssetExifV1> exifs = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
final exifData = payload['exif'];
|
||||
|
||||
if (assetData == null || exifData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final asset = SyncAssetV1.fromJson(assetData);
|
||||
final exif = SyncAssetExifV1.fromJson(exifData);
|
||||
|
||||
if (asset != null && exif != null) {
|
||||
assets.add(asset);
|
||||
exifs.add(exif);
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty && exifs.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
|
||||
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
|
||||
_logger.info('Successfully processed ${assets.length} assets in batch');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,14 @@ class BackgroundSyncManager {
|
||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||
// captured by the closure passed to [runInIsolateGentle].
|
||||
_deviceAlbumSyncTask = full
|
||||
? runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true))
|
||||
: runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false));
|
||||
? runInIsolateGentle(
|
||||
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true),
|
||||
debugLabel: 'local-sync-full-true',
|
||||
)
|
||||
: runInIsolateGentle(
|
||||
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false),
|
||||
debugLabel: 'local-sync-full-false',
|
||||
);
|
||||
|
||||
return _deviceAlbumSyncTask!
|
||||
.whenComplete(() {
|
||||
@@ -122,7 +128,10 @@ class BackgroundSyncManager {
|
||||
|
||||
onHashingStart?.call();
|
||||
|
||||
_hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets());
|
||||
_hashTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||
debugLabel: 'hash-assets',
|
||||
);
|
||||
|
||||
return _hashTask!
|
||||
.whenComplete(() {
|
||||
@@ -142,7 +151,10 @@ class BackgroundSyncManager {
|
||||
|
||||
onRemoteSyncStart?.call();
|
||||
|
||||
_syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync());
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
debugLabel: 'remote-sync',
|
||||
);
|
||||
return _syncTask!
|
||||
.whenComplete(() {
|
||||
onRemoteSyncComplete?.call();
|
||||
@@ -169,7 +181,7 @@ class BackgroundSyncManager {
|
||||
return _linkedAlbumSyncTask!.future;
|
||||
}
|
||||
|
||||
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
|
||||
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
|
||||
return _linkedAlbumSyncTask!.whenComplete(() {
|
||||
_linkedAlbumSyncTask = null;
|
||||
});
|
||||
@@ -178,4 +190,5 @@ class BackgroundSyncManager {
|
||||
|
||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const String kIsolateLockManagerPort = "immich://isolate_mutex";
|
||||
|
||||
enum _LockStatus { active, released }
|
||||
|
||||
class _IsolateRequest {
|
||||
const _IsolateRequest();
|
||||
}
|
||||
|
||||
class _HeartbeatRequest extends _IsolateRequest {
|
||||
// Port for the receiver to send replies back
|
||||
final SendPort sendPort;
|
||||
|
||||
const _HeartbeatRequest(this.sendPort);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'heartbeat', 'sendPort': sendPort};
|
||||
}
|
||||
}
|
||||
|
||||
class _CloseRequest extends _IsolateRequest {
|
||||
const _CloseRequest();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'close'};
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateResponse {
|
||||
const _IsolateResponse();
|
||||
}
|
||||
|
||||
class _HeartbeatResponse extends _IsolateResponse {
|
||||
final _LockStatus status;
|
||||
|
||||
const _HeartbeatResponse(this.status);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'heartbeat', 'status': status.index};
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnCloseLockHolderRequest = void Function();
|
||||
|
||||
class IsolateLockManager {
|
||||
final String _portName;
|
||||
bool _hasLock = false;
|
||||
ReceivePort? _receivePort;
|
||||
final OnCloseLockHolderRequest? _onCloseRequest;
|
||||
final Set<SendPort> _waitingIsolates = {};
|
||||
// Token object - a new one is created for each acquisition attempt
|
||||
Object? _currentAcquisitionToken;
|
||||
|
||||
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
|
||||
: _portName = portName ?? kIsolateLockManagerPort,
|
||||
_onCloseRequest = onCloseRequest;
|
||||
|
||||
Future<bool> acquireLock() async {
|
||||
if (_hasLock) {
|
||||
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a new token - this invalidates any previous attempt
|
||||
final token = _currentAcquisitionToken = Object();
|
||||
|
||||
final ReceivePort rp = _receivePort = ReceivePort(_portName);
|
||||
final SendPort sp = rp.sendPort;
|
||||
|
||||
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
|
||||
// This attempt was superseded by a newer one in the same isolate
|
||||
if (_currentAcquisitionToken != token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _lockReleasedByHolder(token);
|
||||
}
|
||||
|
||||
_hasLock = true;
|
||||
rp.listen(_onRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _lockReleasedByHolder(Object token) async {
|
||||
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
|
||||
debugPrint("Found lock holder: $holder");
|
||||
if (holder == null) {
|
||||
// No holder, try and acquire lock
|
||||
return;
|
||||
}
|
||||
|
||||
final ReceivePort tempRp = ReceivePort();
|
||||
final SendPort tempSp = tempRp.sendPort;
|
||||
final bs = tempRp.asBroadcastStream();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// Send a heartbeat request with the send port to receive reply from the holder
|
||||
|
||||
debugPrint("Sending heartbeat request to lock holder");
|
||||
holder.send(_HeartbeatRequest(tempSp).toJson());
|
||||
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
||||
|
||||
debugPrint("Received heartbeat response from lock holder: $answer");
|
||||
// This attempt was superseded by a newer one in the same isolate
|
||||
if (_currentAcquisitionToken != token) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (answer == null) {
|
||||
// Holder failed, most likely killed without calling releaseLock
|
||||
// Check if a different waiting isolate took the lock
|
||||
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
|
||||
// No, remove the stale lock
|
||||
IsolateNameServer.removePortNameMapping(_portName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Unknown message type received for heartbeat request. Try again
|
||||
_IsolateResponse? response = _parseResponse(answer);
|
||||
if (response == null || response is! _HeartbeatResponse) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.status == _LockStatus.released) {
|
||||
// Holder has released the lock
|
||||
break;
|
||||
}
|
||||
|
||||
// If the _LockStatus is active, we check again if the task completed
|
||||
// by sending a released messaged again, if not, send a new heartbeat again
|
||||
|
||||
// Check if the holder completed its task after the heartbeat
|
||||
answer = await bs.first.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
|
||||
);
|
||||
|
||||
response = _parseResponse(answer);
|
||||
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Timeout or error
|
||||
} finally {
|
||||
tempRp.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_IsolateRequest? _parseRequest(dynamic msg) {
|
||||
if (msg is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (msg['type']) {
|
||||
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
|
||||
'close' => const _CloseRequest(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
_IsolateResponse? _parseResponse(dynamic msg) {
|
||||
if (msg is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (msg['type']) {
|
||||
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Executed in the isolate with the lock
|
||||
void _onRequest(dynamic msg) {
|
||||
final request = _parseRequest(msg);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request is _HeartbeatRequest) {
|
||||
// Add the send port to the list of waiting isolates
|
||||
_waitingIsolates.add(request.sendPort);
|
||||
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
if (request is _CloseRequest) {
|
||||
_onCloseRequest?.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void releaseLock() {
|
||||
if (_hasLock) {
|
||||
IsolateNameServer.removePortNameMapping(_portName);
|
||||
|
||||
// Notify waiting isolates
|
||||
for (final port in _waitingIsolates) {
|
||||
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
|
||||
}
|
||||
_waitingIsolates.clear();
|
||||
|
||||
_hasLock = false;
|
||||
}
|
||||
|
||||
_receivePort?.close();
|
||||
_receivePort = null;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (_hasLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Cancelling ongoing acquire lock attempts");
|
||||
// Create a new token to invalidate ongoing acquire lock attempts
|
||||
_currentAcquisitionToken = Object();
|
||||
}
|
||||
|
||||
void requestHolderToClose() {
|
||||
if (_hasLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
|
||||
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||
final user = ref.read(currentUserProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
if (user == null) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
|
||||
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
|
||||
bool get hasCellular => contains(NetworkCapability.cellular);
|
||||
bool get hasWifi => contains(NetworkCapability.wifi);
|
||||
bool get hasVpn => contains(NetworkCapability.vpn);
|
||||
bool get isUnmetered => contains(NetworkCapability.unmetered);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
extension CurrentPlatform on TargetPlatform {
|
||||
@pragma('vm:prefer-inline')
|
||||
static bool get isIOS => defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
static bool get isAndroid => defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:intl/message_format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
extension StringTranslateExtension on String {
|
||||
String t({BuildContext? context, Map<String, Object>? args}) {
|
||||
@@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map<String, Object>?
|
||||
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args)
|
||||
: translatedMessage;
|
||||
} catch (e) {
|
||||
debugPrint('Translation failed for key "$key". Error: $e');
|
||||
dPrint(() => 'Translation failed for key "$key". Error: $e');
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class AuthUserEntity extends Table with DriftDefaultsMixin {
|
||||
const AuthUserEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get email => text()();
|
||||
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
|
||||
|
||||
// Profile image
|
||||
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
IntColumn get avatarColor => intEnum<AvatarColor>()();
|
||||
|
||||
// Quota
|
||||
IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))();
|
||||
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
|
||||
|
||||
// Locked Folder
|
||||
TextColumn get pinCode => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -0,0 +1,933 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'
|
||||
as i3;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
|
||||
typedef $$AuthUserEntityTableCreateCompanionBuilder =
|
||||
i1.AuthUserEntityCompanion Function({
|
||||
required String id,
|
||||
required String name,
|
||||
required String email,
|
||||
i0.Value<bool> isAdmin,
|
||||
i0.Value<bool> hasProfileImage,
|
||||
i0.Value<DateTime> profileChangedAt,
|
||||
required i2.AvatarColor avatarColor,
|
||||
i0.Value<int> quotaSizeInBytes,
|
||||
i0.Value<int> quotaUsageInBytes,
|
||||
i0.Value<String?> pinCode,
|
||||
});
|
||||
typedef $$AuthUserEntityTableUpdateCompanionBuilder =
|
||||
i1.AuthUserEntityCompanion Function({
|
||||
i0.Value<String> id,
|
||||
i0.Value<String> name,
|
||||
i0.Value<String> email,
|
||||
i0.Value<bool> isAdmin,
|
||||
i0.Value<bool> hasProfileImage,
|
||||
i0.Value<DateTime> profileChangedAt,
|
||||
i0.Value<i2.AvatarColor> avatarColor,
|
||||
i0.Value<int> quotaSizeInBytes,
|
||||
i0.Value<int> quotaUsageInBytes,
|
||||
i0.Value<String?> pinCode,
|
||||
});
|
||||
|
||||
class $$AuthUserEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
|
||||
$$AuthUserEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get name => $composableBuilder(
|
||||
column: $table.name,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
|
||||
column: $table.isAdmin,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<bool> get hasProfileImage => $composableBuilder(
|
||||
column: $table.hasProfileImage,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get profileChangedAt => $composableBuilder(
|
||||
column: $table.profileChangedAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
|
||||
get avatarColor => $composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<int> get quotaSizeInBytes => $composableBuilder(
|
||||
column: $table.quotaSizeInBytes,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<int> get quotaUsageInBytes => $composableBuilder(
|
||||
column: $table.quotaUsageInBytes,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get pinCode => $composableBuilder(
|
||||
column: $table.pinCode,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$AuthUserEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
|
||||
$$AuthUserEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get name => $composableBuilder(
|
||||
column: $table.name,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
|
||||
column: $table.isAdmin,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<bool> get hasProfileImage => $composableBuilder(
|
||||
column: $table.hasProfileImage,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get profileChangedAt => $composableBuilder(
|
||||
column: $table.profileChangedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get quotaSizeInBytes => $composableBuilder(
|
||||
column: $table.quotaSizeInBytes,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get quotaUsageInBytes => $composableBuilder(
|
||||
column: $table.quotaUsageInBytes,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get pinCode => $composableBuilder(
|
||||
column: $table.pinCode,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$AuthUserEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
|
||||
$$AuthUserEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get name =>
|
||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get email =>
|
||||
$composableBuilder(column: $table.email, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get isAdmin =>
|
||||
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get hasProfileImage => $composableBuilder(
|
||||
column: $table.hasProfileImage,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get profileChangedAt => $composableBuilder(
|
||||
column: $table.profileChangedAt,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
|
||||
$composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<int> get quotaSizeInBytes => $composableBuilder(
|
||||
column: $table.quotaSizeInBytes,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<int> get quotaUsageInBytes => $composableBuilder(
|
||||
column: $table.quotaUsageInBytes,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<String> get pinCode =>
|
||||
$composableBuilder(column: $table.pinCode, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$AuthUserEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AuthUserEntityTable,
|
||||
i1.AuthUserEntityData,
|
||||
i1.$$AuthUserEntityTableFilterComposer,
|
||||
i1.$$AuthUserEntityTableOrderingComposer,
|
||||
i1.$$AuthUserEntityTableAnnotationComposer,
|
||||
$$AuthUserEntityTableCreateCompanionBuilder,
|
||||
$$AuthUserEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.AuthUserEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AuthUserEntityTable,
|
||||
i1.AuthUserEntityData
|
||||
>,
|
||||
),
|
||||
i1.AuthUserEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
> {
|
||||
$$AuthUserEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$AuthUserEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$AuthUserEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$AuthUserEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () => i1
|
||||
.$$AuthUserEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<String> name = const i0.Value.absent(),
|
||||
i0.Value<String> email = const i0.Value.absent(),
|
||||
i0.Value<bool> isAdmin = const i0.Value.absent(),
|
||||
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
|
||||
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
|
||||
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
|
||||
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
|
||||
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
|
||||
i0.Value<String?> pinCode = const i0.Value.absent(),
|
||||
}) => i1.AuthUserEntityCompanion(
|
||||
id: id,
|
||||
name: name,
|
||||
email: email,
|
||||
isAdmin: isAdmin,
|
||||
hasProfileImage: hasProfileImage,
|
||||
profileChangedAt: profileChangedAt,
|
||||
avatarColor: avatarColor,
|
||||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes,
|
||||
pinCode: pinCode,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String name,
|
||||
required String email,
|
||||
i0.Value<bool> isAdmin = const i0.Value.absent(),
|
||||
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
|
||||
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
|
||||
required i2.AvatarColor avatarColor,
|
||||
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
|
||||
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
|
||||
i0.Value<String?> pinCode = const i0.Value.absent(),
|
||||
}) => i1.AuthUserEntityCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
email: email,
|
||||
isAdmin: isAdmin,
|
||||
hasProfileImage: hasProfileImage,
|
||||
profileChangedAt: profileChangedAt,
|
||||
avatarColor: avatarColor,
|
||||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes,
|
||||
pinCode: pinCode,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$AuthUserEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AuthUserEntityTable,
|
||||
i1.AuthUserEntityData,
|
||||
i1.$$AuthUserEntityTableFilterComposer,
|
||||
i1.$$AuthUserEntityTableOrderingComposer,
|
||||
i1.$$AuthUserEntityTableAnnotationComposer,
|
||||
$$AuthUserEntityTableCreateCompanionBuilder,
|
||||
$$AuthUserEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.AuthUserEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AuthUserEntityTable,
|
||||
i1.AuthUserEntityData
|
||||
>,
|
||||
),
|
||||
i1.AuthUserEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $AuthUserEntityTable extends i3.AuthUserEntity
|
||||
with i0.TableInfo<$AuthUserEntityTable, i1.AuthUserEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$AuthUserEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta(
|
||||
'name',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
||||
'name',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
|
||||
'email',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> email = i0.GeneratedColumn<String>(
|
||||
'email',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
|
||||
'isAdmin',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
|
||||
'is_admin',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_admin" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i4.Constant(false),
|
||||
);
|
||||
static const i0.VerificationMeta _hasProfileImageMeta =
|
||||
const i0.VerificationMeta('hasProfileImage');
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> hasProfileImage =
|
||||
i0.GeneratedColumn<bool>(
|
||||
'has_profile_image',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("has_profile_image" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i4.Constant(false),
|
||||
);
|
||||
static const i0.VerificationMeta _profileChangedAtMeta =
|
||||
const i0.VerificationMeta('profileChangedAt');
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> profileChangedAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'profile_changed_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i4.currentDateAndTime,
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
|
||||
avatarColor =
|
||||
i0.GeneratedColumn<int>(
|
||||
'avatar_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<i2.AvatarColor>(
|
||||
i1.$AuthUserEntityTable.$converteravatarColor,
|
||||
);
|
||||
static const i0.VerificationMeta _quotaSizeInBytesMeta =
|
||||
const i0.VerificationMeta('quotaSizeInBytes');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> quotaSizeInBytes = i0.GeneratedColumn<int>(
|
||||
'quota_size_in_bytes',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
);
|
||||
static const i0.VerificationMeta _quotaUsageInBytesMeta =
|
||||
const i0.VerificationMeta('quotaUsageInBytes');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> quotaUsageInBytes =
|
||||
i0.GeneratedColumn<int>(
|
||||
'quota_usage_in_bytes',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
);
|
||||
static const i0.VerificationMeta _pinCodeMeta = const i0.VerificationMeta(
|
||||
'pinCode',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> pinCode = i0.GeneratedColumn<String>(
|
||||
'pin_code',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
isAdmin,
|
||||
hasProfileImage,
|
||||
profileChangedAt,
|
||||
avatarColor,
|
||||
quotaSizeInBytes,
|
||||
quotaUsageInBytes,
|
||||
pinCode,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'auth_user_entity';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.AuthUserEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta,
|
||||
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
if (data.containsKey('email')) {
|
||||
context.handle(
|
||||
_emailMeta,
|
||||
email.isAcceptableOrUnknown(data['email']!, _emailMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_emailMeta);
|
||||
}
|
||||
if (data.containsKey('is_admin')) {
|
||||
context.handle(
|
||||
_isAdminMeta,
|
||||
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('has_profile_image')) {
|
||||
context.handle(
|
||||
_hasProfileImageMeta,
|
||||
hasProfileImage.isAcceptableOrUnknown(
|
||||
data['has_profile_image']!,
|
||||
_hasProfileImageMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('profile_changed_at')) {
|
||||
context.handle(
|
||||
_profileChangedAtMeta,
|
||||
profileChangedAt.isAcceptableOrUnknown(
|
||||
data['profile_changed_at']!,
|
||||
_profileChangedAtMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('quota_size_in_bytes')) {
|
||||
context.handle(
|
||||
_quotaSizeInBytesMeta,
|
||||
quotaSizeInBytes.isAcceptableOrUnknown(
|
||||
data['quota_size_in_bytes']!,
|
||||
_quotaSizeInBytesMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('quota_usage_in_bytes')) {
|
||||
context.handle(
|
||||
_quotaUsageInBytesMeta,
|
||||
quotaUsageInBytes.isAcceptableOrUnknown(
|
||||
data['quota_usage_in_bytes']!,
|
||||
_quotaUsageInBytesMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('pin_code')) {
|
||||
context.handle(
|
||||
_pinCodeMeta,
|
||||
pinCode.isAcceptableOrUnknown(data['pin_code']!, _pinCodeMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.AuthUserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.AuthUserEntityData(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
name: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}name'],
|
||||
)!,
|
||||
email: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}email'],
|
||||
)!,
|
||||
isAdmin: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_admin'],
|
||||
)!,
|
||||
hasProfileImage: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}has_profile_image'],
|
||||
)!,
|
||||
profileChangedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}profile_changed_at'],
|
||||
)!,
|
||||
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}avatar_color'],
|
||||
)!,
|
||||
),
|
||||
quotaSizeInBytes: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}quota_size_in_bytes'],
|
||||
)!,
|
||||
quotaUsageInBytes: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}quota_usage_in_bytes'],
|
||||
)!,
|
||||
pinCode: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}pin_code'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$AuthUserEntityTable createAlias(String alias) {
|
||||
return $AuthUserEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
|
||||
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class AuthUserEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.AuthUserEntityData> {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
final bool isAdmin;
|
||||
final bool hasProfileImage;
|
||||
final DateTime profileChangedAt;
|
||||
final i2.AvatarColor avatarColor;
|
||||
final int quotaSizeInBytes;
|
||||
final int quotaUsageInBytes;
|
||||
final String? pinCode;
|
||||
const AuthUserEntityData({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.isAdmin,
|
||||
required this.hasProfileImage,
|
||||
required this.profileChangedAt,
|
||||
required this.avatarColor,
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
this.pinCode,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['name'] = i0.Variable<String>(name);
|
||||
map['email'] = i0.Variable<String>(email);
|
||||
map['is_admin'] = i0.Variable<bool>(isAdmin);
|
||||
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
|
||||
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
|
||||
{
|
||||
map['avatar_color'] = i0.Variable<int>(
|
||||
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor),
|
||||
);
|
||||
}
|
||||
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes);
|
||||
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes);
|
||||
if (!nullToAbsent || pinCode != null) {
|
||||
map['pin_code'] = i0.Variable<String>(pinCode);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory AuthUserEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return AuthUserEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
email: serializer.fromJson<String>(json['email']),
|
||||
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
|
||||
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
|
||||
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
|
||||
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromJson(
|
||||
serializer.fromJson<int>(json['avatarColor']),
|
||||
),
|
||||
quotaSizeInBytes: serializer.fromJson<int>(json['quotaSizeInBytes']),
|
||||
quotaUsageInBytes: serializer.fromJson<int>(json['quotaUsageInBytes']),
|
||||
pinCode: serializer.fromJson<String?>(json['pinCode']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'email': serializer.toJson<String>(email),
|
||||
'isAdmin': serializer.toJson<bool>(isAdmin),
|
||||
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
|
||||
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
|
||||
'avatarColor': serializer.toJson<int>(
|
||||
i1.$AuthUserEntityTable.$converteravatarColor.toJson(avatarColor),
|
||||
),
|
||||
'quotaSizeInBytes': serializer.toJson<int>(quotaSizeInBytes),
|
||||
'quotaUsageInBytes': serializer.toJson<int>(quotaUsageInBytes),
|
||||
'pinCode': serializer.toJson<String?>(pinCode),
|
||||
};
|
||||
}
|
||||
|
||||
i1.AuthUserEntityData copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? email,
|
||||
bool? isAdmin,
|
||||
bool? hasProfileImage,
|
||||
DateTime? profileChangedAt,
|
||||
i2.AvatarColor? avatarColor,
|
||||
int? quotaSizeInBytes,
|
||||
int? quotaUsageInBytes,
|
||||
i0.Value<String?> pinCode = const i0.Value.absent(),
|
||||
}) => i1.AuthUserEntityData(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
|
||||
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
|
||||
pinCode: pinCode.present ? pinCode.value : this.pinCode,
|
||||
);
|
||||
AuthUserEntityData copyWithCompanion(i1.AuthUserEntityCompanion data) {
|
||||
return AuthUserEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
name: data.name.present ? data.name.value : this.name,
|
||||
email: data.email.present ? data.email.value : this.email,
|
||||
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
|
||||
hasProfileImage: data.hasProfileImage.present
|
||||
? data.hasProfileImage.value
|
||||
: this.hasProfileImage,
|
||||
profileChangedAt: data.profileChangedAt.present
|
||||
? data.profileChangedAt.value
|
||||
: this.profileChangedAt,
|
||||
avatarColor: data.avatarColor.present
|
||||
? data.avatarColor.value
|
||||
: this.avatarColor,
|
||||
quotaSizeInBytes: data.quotaSizeInBytes.present
|
||||
? data.quotaSizeInBytes.value
|
||||
: this.quotaSizeInBytes,
|
||||
quotaUsageInBytes: data.quotaUsageInBytes.present
|
||||
? data.quotaUsageInBytes.value
|
||||
: this.quotaUsageInBytes,
|
||||
pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AuthUserEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('email: $email, ')
|
||||
..write('isAdmin: $isAdmin, ')
|
||||
..write('hasProfileImage: $hasProfileImage, ')
|
||||
..write('profileChangedAt: $profileChangedAt, ')
|
||||
..write('avatarColor: $avatarColor, ')
|
||||
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
|
||||
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
|
||||
..write('pinCode: $pinCode')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
isAdmin,
|
||||
hasProfileImage,
|
||||
profileChangedAt,
|
||||
avatarColor,
|
||||
quotaSizeInBytes,
|
||||
quotaUsageInBytes,
|
||||
pinCode,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.AuthUserEntityData &&
|
||||
other.id == this.id &&
|
||||
other.name == this.name &&
|
||||
other.email == this.email &&
|
||||
other.isAdmin == this.isAdmin &&
|
||||
other.hasProfileImage == this.hasProfileImage &&
|
||||
other.profileChangedAt == this.profileChangedAt &&
|
||||
other.avatarColor == this.avatarColor &&
|
||||
other.quotaSizeInBytes == this.quotaSizeInBytes &&
|
||||
other.quotaUsageInBytes == this.quotaUsageInBytes &&
|
||||
other.pinCode == this.pinCode);
|
||||
}
|
||||
|
||||
class AuthUserEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.AuthUserEntityData> {
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> name;
|
||||
final i0.Value<String> email;
|
||||
final i0.Value<bool> isAdmin;
|
||||
final i0.Value<bool> hasProfileImage;
|
||||
final i0.Value<DateTime> profileChangedAt;
|
||||
final i0.Value<i2.AvatarColor> avatarColor;
|
||||
final i0.Value<int> quotaSizeInBytes;
|
||||
final i0.Value<int> quotaUsageInBytes;
|
||||
final i0.Value<String?> pinCode;
|
||||
const AuthUserEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
this.email = const i0.Value.absent(),
|
||||
this.isAdmin = const i0.Value.absent(),
|
||||
this.hasProfileImage = const i0.Value.absent(),
|
||||
this.profileChangedAt = const i0.Value.absent(),
|
||||
this.avatarColor = const i0.Value.absent(),
|
||||
this.quotaSizeInBytes = const i0.Value.absent(),
|
||||
this.quotaUsageInBytes = const i0.Value.absent(),
|
||||
this.pinCode = const i0.Value.absent(),
|
||||
});
|
||||
AuthUserEntityCompanion.insert({
|
||||
required String id,
|
||||
required String name,
|
||||
required String email,
|
||||
this.isAdmin = const i0.Value.absent(),
|
||||
this.hasProfileImage = const i0.Value.absent(),
|
||||
this.profileChangedAt = const i0.Value.absent(),
|
||||
required i2.AvatarColor avatarColor,
|
||||
this.quotaSizeInBytes = const i0.Value.absent(),
|
||||
this.quotaUsageInBytes = const i0.Value.absent(),
|
||||
this.pinCode = const i0.Value.absent(),
|
||||
}) : id = i0.Value(id),
|
||||
name = i0.Value(name),
|
||||
email = i0.Value(email),
|
||||
avatarColor = i0.Value(avatarColor);
|
||||
static i0.Insertable<i1.AuthUserEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? name,
|
||||
i0.Expression<String>? email,
|
||||
i0.Expression<bool>? isAdmin,
|
||||
i0.Expression<bool>? hasProfileImage,
|
||||
i0.Expression<DateTime>? profileChangedAt,
|
||||
i0.Expression<int>? avatarColor,
|
||||
i0.Expression<int>? quotaSizeInBytes,
|
||||
i0.Expression<int>? quotaUsageInBytes,
|
||||
i0.Expression<String>? pinCode,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
if (email != null) 'email': email,
|
||||
if (isAdmin != null) 'is_admin': isAdmin,
|
||||
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
|
||||
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
|
||||
if (avatarColor != null) 'avatar_color': avatarColor,
|
||||
if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes,
|
||||
if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes,
|
||||
if (pinCode != null) 'pin_code': pinCode,
|
||||
});
|
||||
}
|
||||
|
||||
i1.AuthUserEntityCompanion copyWith({
|
||||
i0.Value<String>? id,
|
||||
i0.Value<String>? name,
|
||||
i0.Value<String>? email,
|
||||
i0.Value<bool>? isAdmin,
|
||||
i0.Value<bool>? hasProfileImage,
|
||||
i0.Value<DateTime>? profileChangedAt,
|
||||
i0.Value<i2.AvatarColor>? avatarColor,
|
||||
i0.Value<int>? quotaSizeInBytes,
|
||||
i0.Value<int>? quotaUsageInBytes,
|
||||
i0.Value<String?>? pinCode,
|
||||
}) {
|
||||
return i1.AuthUserEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
|
||||
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
|
||||
pinCode: pinCode ?? this.pinCode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<String>(id.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
if (email.present) {
|
||||
map['email'] = i0.Variable<String>(email.value);
|
||||
}
|
||||
if (isAdmin.present) {
|
||||
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
|
||||
}
|
||||
if (hasProfileImage.present) {
|
||||
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage.value);
|
||||
}
|
||||
if (profileChangedAt.present) {
|
||||
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
|
||||
}
|
||||
if (avatarColor.present) {
|
||||
map['avatar_color'] = i0.Variable<int>(
|
||||
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor.value),
|
||||
);
|
||||
}
|
||||
if (quotaSizeInBytes.present) {
|
||||
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes.value);
|
||||
}
|
||||
if (quotaUsageInBytes.present) {
|
||||
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes.value);
|
||||
}
|
||||
if (pinCode.present) {
|
||||
map['pin_code'] = i0.Variable<String>(pinCode.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AuthUserEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('email: $email, ')
|
||||
..write('isAdmin: $isAdmin, ')
|
||||
..write('hasProfileImage: $hasProfileImage, ')
|
||||
..write('profileChangedAt: $profileChangedAt, ')
|
||||
..write('avatarColor: $avatarColor, ')
|
||||
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
|
||||
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
|
||||
..write('pinCode: $pinCode')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:drift/drift.dart' hide Index;
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
@@ -44,7 +43,7 @@ class User {
|
||||
|
||||
static User fromDto(UserDto dto) => User(
|
||||
id: dto.id,
|
||||
updatedAt: dto.updatedAt,
|
||||
updatedAt: dto.updatedAt ?? DateTime(2025),
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
isAdmin: dto.isAdmin,
|
||||
@@ -81,13 +80,12 @@ class UserEntity extends Table with DriftDefaultsMixin {
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
|
||||
TextColumn get email => text()();
|
||||
|
||||
// Profile image
|
||||
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
IntColumn get avatarColor => intEnum<AvatarColor>().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
||||
+74
-129
@@ -3,28 +3,27 @@
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
|
||||
typedef $$UserEntityTableCreateCompanionBuilder =
|
||||
i1.UserEntityCompanion Function({
|
||||
required String id,
|
||||
required String name,
|
||||
i0.Value<bool> isAdmin,
|
||||
required String email,
|
||||
i0.Value<bool> hasProfileImage,
|
||||
i0.Value<DateTime> profileChangedAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<i2.AvatarColor> avatarColor,
|
||||
});
|
||||
typedef $$UserEntityTableUpdateCompanionBuilder =
|
||||
i1.UserEntityCompanion Function({
|
||||
i0.Value<String> id,
|
||||
i0.Value<String> name,
|
||||
i0.Value<bool> isAdmin,
|
||||
i0.Value<String> email,
|
||||
i0.Value<bool> hasProfileImage,
|
||||
i0.Value<DateTime> profileChangedAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<i2.AvatarColor> avatarColor,
|
||||
});
|
||||
|
||||
class $$UserEntityTableFilterComposer
|
||||
@@ -46,11 +45,6 @@ class $$UserEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
|
||||
column: $table.isAdmin,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
@@ -66,9 +60,10 @@ class $$UserEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
|
||||
get avatarColor => $composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,11 +86,6 @@ class $$UserEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
|
||||
column: $table.isAdmin,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
@@ -111,8 +101,8 @@ class $$UserEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
@@ -132,9 +122,6 @@ class $$UserEntityTableAnnotationComposer
|
||||
i0.GeneratedColumn<String> get name =>
|
||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get isAdmin =>
|
||||
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get email =>
|
||||
$composableBuilder(column: $table.email, builder: (column) => column);
|
||||
|
||||
@@ -148,8 +135,11 @@ class $$UserEntityTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
|
||||
$composableBuilder(
|
||||
column: $table.avatarColor,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$UserEntityTableTableManager
|
||||
@@ -191,37 +181,33 @@ class $$UserEntityTableTableManager
|
||||
({
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<String> name = const i0.Value.absent(),
|
||||
i0.Value<bool> isAdmin = const i0.Value.absent(),
|
||||
i0.Value<String> email = const i0.Value.absent(),
|
||||
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
|
||||
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
|
||||
}) => i1.UserEntityCompanion(
|
||||
id: id,
|
||||
name: name,
|
||||
isAdmin: isAdmin,
|
||||
email: email,
|
||||
hasProfileImage: hasProfileImage,
|
||||
profileChangedAt: profileChangedAt,
|
||||
updatedAt: updatedAt,
|
||||
avatarColor: avatarColor,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String name,
|
||||
i0.Value<bool> isAdmin = const i0.Value.absent(),
|
||||
required String email,
|
||||
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
|
||||
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
|
||||
}) => i1.UserEntityCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
isAdmin: isAdmin,
|
||||
email: email,
|
||||
hasProfileImage: hasProfileImage,
|
||||
profileChangedAt: profileChangedAt,
|
||||
updatedAt: updatedAt,
|
||||
avatarColor: avatarColor,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
@@ -253,7 +239,7 @@ typedef $$UserEntityTableProcessedTableManager =
|
||||
i0.PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $UserEntityTable extends i2.UserEntity
|
||||
class $UserEntityTable extends i3.UserEntity
|
||||
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
@@ -279,21 +265,6 @@ class $UserEntityTable extends i2.UserEntity
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
|
||||
'isAdmin',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
|
||||
'is_admin',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_admin" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i3.Constant(false),
|
||||
);
|
||||
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
|
||||
'email',
|
||||
);
|
||||
@@ -318,7 +289,7 @@ class $UserEntityTable extends i2.UserEntity
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("has_profile_image" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const i3.Constant(false),
|
||||
defaultValue: const i4.Constant(false),
|
||||
);
|
||||
static const i0.VerificationMeta _profileChangedAtMeta =
|
||||
const i0.VerificationMeta('profileChangedAt');
|
||||
@@ -330,30 +301,26 @@ class $UserEntityTable extends i2.UserEntity
|
||||
false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i3.currentDateAndTime,
|
||||
defaultValue: i4.currentDateAndTime,
|
||||
);
|
||||
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i3.currentDateAndTime,
|
||||
);
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
|
||||
avatarColor = i0.GeneratedColumn<int>(
|
||||
'avatar_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
).withConverter<i2.AvatarColor>(i1.$UserEntityTable.$converteravatarColor);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
id,
|
||||
name,
|
||||
isAdmin,
|
||||
email,
|
||||
hasProfileImage,
|
||||
profileChangedAt,
|
||||
updatedAt,
|
||||
avatarColor,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -380,12 +347,6 @@ class $UserEntityTable extends i2.UserEntity
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
if (data.containsKey('is_admin')) {
|
||||
context.handle(
|
||||
_isAdminMeta,
|
||||
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('email')) {
|
||||
context.handle(
|
||||
_emailMeta,
|
||||
@@ -412,12 +373,6 @@ class $UserEntityTable extends i2.UserEntity
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -435,10 +390,6 @@ class $UserEntityTable extends i2.UserEntity
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}name'],
|
||||
)!,
|
||||
isAdmin: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_admin'],
|
||||
)!,
|
||||
email: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}email'],
|
||||
@@ -451,10 +402,12 @@ class $UserEntityTable extends i2.UserEntity
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}profile_changed_at'],
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}avatar_color'],
|
||||
)!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -463,6 +416,8 @@ class $UserEntityTable extends i2.UserEntity
|
||||
return $UserEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
|
||||
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
@@ -473,30 +428,31 @@ class UserEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.UserEntityData> {
|
||||
final String id;
|
||||
final String name;
|
||||
final bool isAdmin;
|
||||
final String email;
|
||||
final bool hasProfileImage;
|
||||
final DateTime profileChangedAt;
|
||||
final DateTime updatedAt;
|
||||
final i2.AvatarColor avatarColor;
|
||||
const UserEntityData({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.isAdmin,
|
||||
required this.email,
|
||||
required this.hasProfileImage,
|
||||
required this.profileChangedAt,
|
||||
required this.updatedAt,
|
||||
required this.avatarColor,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['name'] = i0.Variable<String>(name);
|
||||
map['is_admin'] = i0.Variable<bool>(isAdmin);
|
||||
map['email'] = i0.Variable<String>(email);
|
||||
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
|
||||
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
{
|
||||
map['avatar_color'] = i0.Variable<int>(
|
||||
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -508,11 +464,12 @@ class UserEntityData extends i0.DataClass
|
||||
return UserEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
|
||||
email: serializer.fromJson<String>(json['email']),
|
||||
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
|
||||
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromJson(
|
||||
serializer.fromJson<int>(json['avatarColor']),
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -521,36 +478,34 @@ class UserEntityData extends i0.DataClass
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'isAdmin': serializer.toJson<bool>(isAdmin),
|
||||
'email': serializer.toJson<String>(email),
|
||||
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
|
||||
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'avatarColor': serializer.toJson<int>(
|
||||
i1.$UserEntityTable.$converteravatarColor.toJson(avatarColor),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
i1.UserEntityData copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
bool? isAdmin,
|
||||
String? email,
|
||||
bool? hasProfileImage,
|
||||
DateTime? profileChangedAt,
|
||||
DateTime? updatedAt,
|
||||
i2.AvatarColor? avatarColor,
|
||||
}) => i1.UserEntityData(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
email: email ?? this.email,
|
||||
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
|
||||
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
);
|
||||
UserEntityData copyWithCompanion(i1.UserEntityCompanion data) {
|
||||
return UserEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
name: data.name.present ? data.name.value : this.name,
|
||||
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
|
||||
email: data.email.present ? data.email.value : this.email,
|
||||
hasProfileImage: data.hasProfileImage.present
|
||||
? data.hasProfileImage.value
|
||||
@@ -558,7 +513,9 @@ class UserEntityData extends i0.DataClass
|
||||
profileChangedAt: data.profileChangedAt.present
|
||||
? data.profileChangedAt.value
|
||||
: this.profileChangedAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
avatarColor: data.avatarColor.present
|
||||
? data.avatarColor.value
|
||||
: this.avatarColor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -567,11 +524,10 @@ class UserEntityData extends i0.DataClass
|
||||
return (StringBuffer('UserEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('isAdmin: $isAdmin, ')
|
||||
..write('email: $email, ')
|
||||
..write('hasProfileImage: $hasProfileImage, ')
|
||||
..write('profileChangedAt: $profileChangedAt, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write('avatarColor: $avatarColor')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -580,11 +536,10 @@ class UserEntityData extends i0.DataClass
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
name,
|
||||
isAdmin,
|
||||
email,
|
||||
hasProfileImage,
|
||||
profileChangedAt,
|
||||
updatedAt,
|
||||
avatarColor,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -592,78 +547,70 @@ class UserEntityData extends i0.DataClass
|
||||
(other is i1.UserEntityData &&
|
||||
other.id == this.id &&
|
||||
other.name == this.name &&
|
||||
other.isAdmin == this.isAdmin &&
|
||||
other.email == this.email &&
|
||||
other.hasProfileImage == this.hasProfileImage &&
|
||||
other.profileChangedAt == this.profileChangedAt &&
|
||||
other.updatedAt == this.updatedAt);
|
||||
other.avatarColor == this.avatarColor);
|
||||
}
|
||||
|
||||
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> name;
|
||||
final i0.Value<bool> isAdmin;
|
||||
final i0.Value<String> email;
|
||||
final i0.Value<bool> hasProfileImage;
|
||||
final i0.Value<DateTime> profileChangedAt;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
final i0.Value<i2.AvatarColor> avatarColor;
|
||||
const UserEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
this.isAdmin = const i0.Value.absent(),
|
||||
this.email = const i0.Value.absent(),
|
||||
this.hasProfileImage = const i0.Value.absent(),
|
||||
this.profileChangedAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
this.avatarColor = const i0.Value.absent(),
|
||||
});
|
||||
UserEntityCompanion.insert({
|
||||
required String id,
|
||||
required String name,
|
||||
this.isAdmin = const i0.Value.absent(),
|
||||
required String email,
|
||||
this.hasProfileImage = const i0.Value.absent(),
|
||||
this.profileChangedAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
this.avatarColor = const i0.Value.absent(),
|
||||
}) : id = i0.Value(id),
|
||||
name = i0.Value(name),
|
||||
email = i0.Value(email);
|
||||
static i0.Insertable<i1.UserEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? name,
|
||||
i0.Expression<bool>? isAdmin,
|
||||
i0.Expression<String>? email,
|
||||
i0.Expression<bool>? hasProfileImage,
|
||||
i0.Expression<DateTime>? profileChangedAt,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
i0.Expression<int>? avatarColor,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
if (isAdmin != null) 'is_admin': isAdmin,
|
||||
if (email != null) 'email': email,
|
||||
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
|
||||
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (avatarColor != null) 'avatar_color': avatarColor,
|
||||
});
|
||||
}
|
||||
|
||||
i1.UserEntityCompanion copyWith({
|
||||
i0.Value<String>? id,
|
||||
i0.Value<String>? name,
|
||||
i0.Value<bool>? isAdmin,
|
||||
i0.Value<String>? email,
|
||||
i0.Value<bool>? hasProfileImage,
|
||||
i0.Value<DateTime>? profileChangedAt,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
i0.Value<i2.AvatarColor>? avatarColor,
|
||||
}) {
|
||||
return i1.UserEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
email: email ?? this.email,
|
||||
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
|
||||
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -676,9 +623,6 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
if (isAdmin.present) {
|
||||
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
|
||||
}
|
||||
if (email.present) {
|
||||
map['email'] = i0.Variable<String>(email.value);
|
||||
}
|
||||
@@ -688,8 +632,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
|
||||
if (profileChangedAt.present) {
|
||||
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
if (avatarColor.present) {
|
||||
map['avatar_color'] = i0.Variable<int>(
|
||||
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor.value),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -699,11 +645,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
|
||||
return (StringBuffer('UserEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('isAdmin: $isAdmin, ')
|
||||
..write('email: $email, ')
|
||||
..write('hasProfileImage: $hasProfileImage, ')
|
||||
..write('profileChangedAt: $profileChangedAt, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write('avatarColor: $avatarColor')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
@@ -138,22 +137,4 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
|
||||
return query.map((localAsset) => localAsset.toDto()).get();
|
||||
}
|
||||
|
||||
FutureOr<List<LocalAlbum>> getSourceAlbums(String localAssetId) {
|
||||
final query = _db.localAlbumEntity.select()
|
||||
..where(
|
||||
(lae) =>
|
||||
existsQuery(
|
||||
_db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.albumId])
|
||||
..where(
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
|
||||
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
|
||||
),
|
||||
) &
|
||||
lae.backupSelection.equalsValue(BackupSelection.selected),
|
||||
)
|
||||
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
|
||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||
@@ -43,6 +44,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
AuthUserEntity,
|
||||
UserEntity,
|
||||
UserMetadataEntity,
|
||||
PartnerEntity,
|
||||
@@ -67,8 +69,31 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
Drift([QueryExecutor? executor])
|
||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||
|
||||
Future<void> reset() async {
|
||||
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
|
||||
await exclusively(() async {
|
||||
// https://stackoverflow.com/a/65743498/25690041
|
||||
await customStatement('PRAGMA writable_schema = 1;');
|
||||
await customStatement('DELETE FROM sqlite_master;');
|
||||
await customStatement('VACUUM;');
|
||||
await customStatement('PRAGMA writable_schema = 0;');
|
||||
await customStatement('PRAGMA integrity_check');
|
||||
|
||||
await customStatement('PRAGMA user_version = 0');
|
||||
await beforeOpen(
|
||||
// ignore: invalid_use_of_internal_member
|
||||
resolvedEngine.executor,
|
||||
OpeningDetails(null, schemaVersion),
|
||||
);
|
||||
await customStatement('PRAGMA user_version = $schemaVersion');
|
||||
|
||||
// Refresh all stream queries
|
||||
notifyUpdates({for (final table in allTables) TableUpdate.onTable(table)});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 9;
|
||||
int get schemaVersion => 10;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -126,6 +151,11 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
from8To9: (m, v9) async {
|
||||
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||
},
|
||||
from9To10: (m, v10) async {
|
||||
await m.createTable(v10.authUserEntity);
|
||||
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
||||
await m.alterTable(TableMigration(v10.userEntity));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -141,7 +171,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
await customStatement('PRAGMA synchronous = NORMAL');
|
||||
await customStatement('PRAGMA journal_mode = WAL');
|
||||
await customStatement('PRAGMA busy_timeout = 500');
|
||||
await customStatement('PRAGMA busy_timeout = 30000');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+58
-50
@@ -15,29 +15,31 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i10;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i11;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i14;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i15;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
as i16;
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
as i17;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
as i18;
|
||||
import 'package:drift/internal/modular.dart' as i19;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i19;
|
||||
import 'package:drift/internal/modular.dart' as i20;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -54,27 +56,30 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
.$LocalAlbumEntityTable(this);
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
|
||||
.$LocalAlbumAssetEntityTable(this);
|
||||
late final i8.$UserMetadataEntityTable userMetadataEntity = i8
|
||||
.$UserMetadataEntityTable(this);
|
||||
late final i9.$PartnerEntityTable partnerEntity = i9.$PartnerEntityTable(
|
||||
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i10.$RemoteExifEntityTable remoteExifEntity = i10
|
||||
.$RemoteExifEntityTable(this);
|
||||
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
|
||||
.$RemoteAlbumUserEntityTable(this);
|
||||
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
|
||||
late final i14.$MemoryAssetEntityTable memoryAssetEntity = i14
|
||||
.$MemoryAssetEntityTable(this);
|
||||
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
|
||||
late final i16.$AssetFaceEntityTable assetFaceEntity = i16
|
||||
.$AssetFaceEntityTable(this);
|
||||
late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this);
|
||||
i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer(
|
||||
late final i9.$UserMetadataEntityTable userMetadataEntity = i9
|
||||
.$UserMetadataEntityTable(this);
|
||||
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
|
||||
this,
|
||||
).accessor<i18.MergedAssetDrift>(i18.MergedAssetDrift.new);
|
||||
);
|
||||
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
|
||||
.$RemoteExifEntityTable(this);
|
||||
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
|
||||
.$RemoteAlbumUserEntityTable(this);
|
||||
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
|
||||
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
|
||||
.$MemoryAssetEntityTable(this);
|
||||
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
|
||||
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
|
||||
.$AssetFaceEntityTable(this);
|
||||
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
|
||||
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -92,6 +97,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i2.uQRemoteAssetsOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
||||
i2.idxRemoteAssetChecksum,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
@@ -102,7 +108,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
i10.idxLatLng,
|
||||
i11.idxLatLng,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
@@ -305,27 +311,29 @@ class $DriftManager {
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i8.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i8.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i9.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i9.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i10.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i10.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
i8.$$AuthUserEntityTableTableManager get authUserEntity =>
|
||||
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
|
||||
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i10.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.remoteAlbumAssetEntity,
|
||||
);
|
||||
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
|
||||
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
|
||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||
i13.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i15.$$PersonEntityTableTableManager get personEntity =>
|
||||
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i17.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i17.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i14.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i16.$$PersonEntityTableTableManager get personEntity =>
|
||||
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i18.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
}
|
||||
|
||||
@@ -3820,6 +3820,456 @@ i1.GeneratedColumn<String> _column_90(String aliasedName) =>
|
||||
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
|
||||
),
|
||||
);
|
||||
|
||||
final class Schema10 extends i0.VersionedSchema {
|
||||
Schema10({required super.database}) : super(version: 10);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAssetChecksum,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
idxLatLng,
|
||||
];
|
||||
late final Shape20 userEntity = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_91,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape17 remoteAssetEntity = Shape17(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_86,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 stackEntity = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 localAssetEntity = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape9 remoteAlbumEntity = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_56,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape19 localAlbumEntity = Shape19(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_5,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_90,
|
||||
_column_33,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 localAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_34, _column_35],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
late final Shape21 authUserEntity = Shape21(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_2,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_92,
|
||||
_column_93,
|
||||
_column_7,
|
||||
_column_94,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_25, _column_26, _column_27],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape5 partnerEntity = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_28, _column_29, _column_30],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape8 remoteExifEntity = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_11,
|
||||
_column_10,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_36, _column_60],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_60, _column_25, _column_61],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 memoryEntity = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_18,
|
||||
_column_15,
|
||||
_column_8,
|
||||
_column_62,
|
||||
_column_63,
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_36, _column_68],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape14 personEntity = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_1,
|
||||
_column_69,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_73,
|
||||
_column_74,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape15 assetFaceEntity = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_87, _column_88, _column_89],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape20 extends i0.VersionedTable {
|
||||
Shape20({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get email =>
|
||||
columnsByName['email']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get hasProfileImage =>
|
||||
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<DateTime> get profileChangedAt =>
|
||||
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get avatarColor =>
|
||||
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_91(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'avatar_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
|
||||
class Shape21 extends i0.VersionedTable {
|
||||
Shape21({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get email =>
|
||||
columnsByName['email']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isAdmin =>
|
||||
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get hasProfileImage =>
|
||||
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<DateTime> get profileChangedAt =>
|
||||
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get avatarColor =>
|
||||
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get quotaSizeInBytes =>
|
||||
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get quotaUsageInBytes =>
|
||||
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get pinCode =>
|
||||
columnsByName['pin_code']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_92(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'avatar_color',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_93(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'quota_size_in_bytes',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_94(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'pin_code',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -3829,6 +4279,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -3872,6 +4323,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from8To9(migrator, schema);
|
||||
return 9;
|
||||
case 9:
|
||||
final schema = Schema10(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from9To10(migrator, schema);
|
||||
return 10;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -3887,6 +4343,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -3897,5 +4354,6 @@ i1.OnUpgrade stepByStep({
|
||||
from6To7: from6To7,
|
||||
from7To8: from7To8,
|
||||
from8To9: from8To9,
|
||||
from9To10: from9To10,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||
|
||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
final Platform _platform;
|
||||
const DriftLocalAlbumRepository(this._db, {Platform? platform})
|
||||
: _platform = platform ?? const LocalPlatform(),
|
||||
super(_db);
|
||||
|
||||
const DriftLocalAlbumRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
|
||||
final assetCount = _db.localAlbumAssetEntity.assetId.count();
|
||||
@@ -61,7 +59,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
// Remove all assets that are only in this particular album
|
||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||
// That is not the case on Android since asset <-> album has one:one mapping
|
||||
final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
|
||||
final assetsToDelete = CurrentPlatform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
|
||||
await _deleteAssets(assetsToDelete);
|
||||
|
||||
await _db.managers.localAlbumEntity
|
||||
@@ -144,7 +142,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
});
|
||||
|
||||
if (_platform.isAndroid) {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
// On Android, an asset can only be in one album
|
||||
// So, get the albums that are marked for deletion
|
||||
// and delete all the assets that are in those albums
|
||||
@@ -265,7 +263,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
if (_platform.isAndroid) {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
return _deleteAssets(assetIds);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
@@ -26,6 +28,12 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
Future<LocalAsset?> get(String id) => _assetSelectable(id).getSingleOrNull();
|
||||
|
||||
Future<List<LocalAsset?>> getByChecksum(String checksum) {
|
||||
final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum));
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull();
|
||||
|
||||
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
||||
@@ -69,4 +77,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
Future<int> getHashedCount() {
|
||||
return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
final query = _db.localAlbumEntity.select()
|
||||
..where(
|
||||
(lae) => existsQuery(
|
||||
_db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.albumId])
|
||||
..where(
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
|
||||
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
|
||||
),
|
||||
),
|
||||
)
|
||||
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
|
||||
if (backupSelection != null) {
|
||||
query.where((lae) => lae.backupSelection.equalsValue(backupSelection));
|
||||
}
|
||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,14 +202,13 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
isAdmin: user.isAdmin,
|
||||
updatedAt: user.updatedAt,
|
||||
memoryEnabled: true,
|
||||
inTimeline: false,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
profileChangedAt: user.profileChangedAt,
|
||||
hasProfileImage: user.hasProfileImage,
|
||||
avatarColor: user.avatarColor,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
|
||||
@@ -55,6 +55,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
return _assetSelectable(id).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getByChecksum(String checksum) {
|
||||
final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum));
|
||||
|
||||
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||
if (asset.stackId == null) {
|
||||
return Future.value([]);
|
||||
|
||||
@@ -173,7 +173,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
|
||||
const (bool) => entity.intValue == 1,
|
||||
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
||||
const (UserDto) =>
|
||||
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
|
||||
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
|
||||
_ => null,
|
||||
}
|
||||
as T?;
|
||||
@@ -184,7 +184,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
|
||||
const (String) => (null, value as String),
|
||||
const (bool) => ((value as bool) ? 1 : 0, null),
|
||||
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
||||
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
|
||||
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
|
||||
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
||||
};
|
||||
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -18,7 +20,8 @@ class SyncApiRepository {
|
||||
}
|
||||
|
||||
Future<void> streamChanges(
|
||||
Function(List<SyncEvent>, Function() abort) onData, {
|
||||
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
||||
Function()? onReset,
|
||||
int batchSize = kSyncEventBatchSize,
|
||||
http.Client? httpClient,
|
||||
}) async {
|
||||
@@ -32,11 +35,13 @@ class SyncApiRepository {
|
||||
await _api.applyToParams([], headerParams);
|
||||
headers.addAll(headerParams);
|
||||
|
||||
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
|
||||
final request = http.Request('POST', Uri.parse(endpoint));
|
||||
request.headers.addAll(headers);
|
||||
request.body = jsonEncode(
|
||||
SyncStreamDto(
|
||||
types: [
|
||||
SyncRequestType.authUsersV1,
|
||||
SyncRequestType.usersV1,
|
||||
SyncRequestType.assetsV1,
|
||||
SyncRequestType.assetExifsV1,
|
||||
@@ -56,6 +61,7 @@ class SyncApiRepository {
|
||||
SyncRequestType.peopleV1,
|
||||
SyncRequestType.assetFacesV1,
|
||||
],
|
||||
reset: shouldReset,
|
||||
).toJson(),
|
||||
);
|
||||
|
||||
@@ -69,6 +75,8 @@ class SyncApiRepository {
|
||||
shouldAbort = true;
|
||||
}
|
||||
|
||||
final reset = onReset ?? () {};
|
||||
|
||||
try {
|
||||
final response = await client.send(request);
|
||||
|
||||
@@ -77,6 +85,9 @@ class SyncApiRepository {
|
||||
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
|
||||
}
|
||||
|
||||
// Reset after successful stream start
|
||||
await Store.put(StoreKey.shouldResetSync, false);
|
||||
|
||||
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
||||
if (shouldAbort) {
|
||||
break;
|
||||
@@ -91,12 +102,12 @@ class SyncApiRepository {
|
||||
continue;
|
||||
}
|
||||
|
||||
await onData(_parseLines(lines), abort);
|
||||
await onData(_parseLines(lines), abort, reset);
|
||||
lines.clear();
|
||||
}
|
||||
|
||||
if (lines.isNotEmpty && !shouldAbort) {
|
||||
await onData(_parseLines(lines), abort);
|
||||
await onData(_parseLines(lines), abort, reset);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Error processing stream", error, stack);
|
||||
@@ -130,6 +141,7 @@ class SyncApiRepository {
|
||||
}
|
||||
|
||||
const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.authUserV1: SyncAuthUserV1.fromJson,
|
||||
SyncEntityType.userV1: SyncUserV1.fromJson,
|
||||
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
||||
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||
@@ -156,7 +168,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
||||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
||||
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
||||
SyncEntityType.syncAckV1: _SyncEmptyDto.fromJson,
|
||||
SyncEntityType.syncResetV1: _SyncEmptyDto.fromJson,
|
||||
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
|
||||
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
||||
@@ -172,8 +185,9 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||
};
|
||||
|
||||
class _SyncAckV1 {
|
||||
static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1();
|
||||
class _SyncEmptyDto {
|
||||
static _SyncEmptyDto? fromJson(dynamic _) => _SyncEmptyDto();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||
@@ -29,6 +32,65 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
|
||||
SyncStreamRepository(super.db) : _db = db;
|
||||
|
||||
Future<void> reset() async {
|
||||
_logger.fine("SyncResetV1 received. Resetting remote entities");
|
||||
try {
|
||||
await _db.exclusively(() async {
|
||||
// foreign_keys PRAGMA is no-op within transactions
|
||||
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
|
||||
await _db.customStatement('PRAGMA foreign_keys = OFF');
|
||||
await transaction(() async {
|
||||
await _db.assetFaceEntity.deleteAll();
|
||||
await _db.memoryAssetEntity.deleteAll();
|
||||
await _db.memoryEntity.deleteAll();
|
||||
await _db.partnerEntity.deleteAll();
|
||||
await _db.personEntity.deleteAll();
|
||||
await _db.remoteAlbumAssetEntity.deleteAll();
|
||||
await _db.remoteAlbumEntity.deleteAll();
|
||||
await _db.remoteAlbumUserEntity.deleteAll();
|
||||
await _db.remoteAssetEntity.deleteAll();
|
||||
await _db.remoteExifEntity.deleteAll();
|
||||
await _db.stackEntity.deleteAll();
|
||||
await _db.userEntity.deleteAll();
|
||||
await _db.userMetadataEntity.deleteAll();
|
||||
});
|
||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: SyncResetV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAuthUsersV1(Iterable<SyncAuthUserV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final user in data) {
|
||||
final companion = AuthUserEntityCompanion(
|
||||
name: Value(user.name),
|
||||
email: Value(user.email),
|
||||
hasProfileImage: Value(user.hasProfileImage),
|
||||
profileChangedAt: Value(user.profileChangedAt),
|
||||
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
pinCode: Value(user.pinCode),
|
||||
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
|
||||
quotaUsageInBytes: Value(user.quotaUsageInBytes),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.authUserEntity,
|
||||
companion.copyWith(id: Value(user.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: SyncAuthUserV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
||||
try {
|
||||
await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
|
||||
@@ -47,6 +109,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
email: Value(user.email),
|
||||
hasProfileImage: Value(user.hasProfileImage),
|
||||
profileChangedAt: Value(user.profileChangedAt),
|
||||
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
|
||||
);
|
||||
|
||||
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
|
||||
@@ -573,3 +636,7 @@ extension on String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on UserAvatarColor {
|
||||
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
|
||||
}
|
||||
|
||||
@@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
|
||||
}
|
||||
|
||||
return _db.mergedAssetDrift
|
||||
.mergedBucket(userIds: userIds, groupBy: groupBy.index)
|
||||
.map((row) {
|
||||
final date = row.bucketDate.dateFmt(groupBy);
|
||||
return TimeBucket(date: date, assetCount: row.assetCount);
|
||||
})
|
||||
.watch()
|
||||
.throttle(const Duration(seconds: 3), trailing: true);
|
||||
return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
|
||||
final date = row.bucketDate.dateFmt(groupBy);
|
||||
return TimeBucket(date: date, assetCount: row.assetCount);
|
||||
}).watch();
|
||||
}
|
||||
|
||||
Future<List<BaseAsset>> _getMainBucketAssets(List<String> userIds, {required int offset, required int count}) {
|
||||
@@ -595,7 +591,7 @@ extension on String {
|
||||
GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"),
|
||||
};
|
||||
try {
|
||||
return DateFormat(format).parse(this);
|
||||
return DateFormat(format, 'en').parse(this);
|
||||
} catch (e) {
|
||||
throw FormatException("Invalid date format: $this", e);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
@@ -68,12 +68,12 @@ class IsarUserRepository extends IsarDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
class DriftUserRepository extends DriftDatabaseRepository {
|
||||
class DriftAuthUserRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftUserRepository(super.db) : _db = db;
|
||||
const DriftAuthUserRepository(super.db) : _db = db;
|
||||
|
||||
Future<UserDto?> get(String id) async {
|
||||
final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
|
||||
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
|
||||
|
||||
if (user == null) return null;
|
||||
|
||||
@@ -84,43 +84,30 @@ class DriftUserRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<UserDto> upsert(UserDto user) async {
|
||||
await _db.userEntity.insertOnConflictUpdate(
|
||||
UserEntityCompanion(
|
||||
await _db.authUserEntity.insertOnConflictUpdate(
|
||||
AuthUserEntityCompanion(
|
||||
id: Value(user.id),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
updatedAt: Value(user.updatedAt),
|
||||
name: Value(user.name),
|
||||
email: Value(user.email),
|
||||
hasProfileImage: Value(user.hasProfileImage),
|
||||
profileChangedAt: Value(user.profileChangedAt),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
quotaSizeInBytes: Value(user.quotaSizeInBytes),
|
||||
quotaUsageInBytes: Value(user.quotaUsageInBytes),
|
||||
avatarColor: Value(user.avatarColor),
|
||||
),
|
||||
);
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getAll() async {
|
||||
final users = await _db.userEntity.select().get();
|
||||
final List<UserDto> result = [];
|
||||
|
||||
for (final user in users) {
|
||||
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
|
||||
final metadata = await query.map((row) => row.toDto()).get();
|
||||
result.add(user.toDto(metadata));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
extension on UserEntityData {
|
||||
extension on AuthUserEntityData {
|
||||
UserDto toDto([List<UserMetadata>? metadata]) {
|
||||
AvatarColor avatarColor = AvatarColor.primary;
|
||||
bool memoryEnabled = true;
|
||||
|
||||
if (metadata != null) {
|
||||
for (final meta in metadata) {
|
||||
if (meta.key == UserMetadataKey.preferences && meta.preferences != null) {
|
||||
avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary;
|
||||
memoryEnabled = meta.preferences?.memoriesEnabled ?? true;
|
||||
}
|
||||
}
|
||||
@@ -130,12 +117,13 @@ extension on UserEntityData {
|
||||
id: id,
|
||||
email: email,
|
||||
name: name,
|
||||
isAdmin: isAdmin,
|
||||
updatedAt: updatedAt,
|
||||
profileChangedAt: profileChangedAt,
|
||||
hasProfileImage: hasProfileImage,
|
||||
avatarColor: avatarColor,
|
||||
memoryEnabled: memoryEnabled,
|
||||
isAdmin: isAdmin,
|
||||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
// TODO: Move to repository once all classes are refactored
|
||||
|
||||
+13
-12
@@ -17,9 +17,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
@@ -39,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
void main() async {
|
||||
ImmichWidgetsBinding();
|
||||
@@ -69,9 +70,9 @@ Future<void> initApp() async {
|
||||
if (kReleaseMode && Platform.isAndroid) {
|
||||
try {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
debugPrint("Enabled high refresh mode");
|
||||
dPrint(() => "Enabled high refresh mode");
|
||||
} catch (e) {
|
||||
debugPrint("Error setting high refresh rate: $e");
|
||||
dPrint(() => "Error setting high refresh rate: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,23 +127,23 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
debugPrint("[APP STATE] resumed");
|
||||
dPrint(() => "[APP STATE] resumed");
|
||||
ref.read(appStateProvider.notifier).handleAppResume();
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
debugPrint("[APP STATE] inactive");
|
||||
dPrint(() => "[APP STATE] inactive");
|
||||
ref.read(appStateProvider.notifier).handleAppInactivity();
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
debugPrint("[APP STATE] paused");
|
||||
dPrint(() => "[APP STATE] paused");
|
||||
ref.read(appStateProvider.notifier).handleAppPause();
|
||||
break;
|
||||
case AppLifecycleState.detached:
|
||||
debugPrint("[APP STATE] detached");
|
||||
dPrint(() => "[APP STATE] detached");
|
||||
ref.read(appStateProvider.notifier).handleAppDetached();
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
debugPrint("[APP STATE] hidden");
|
||||
dPrint(() => "[APP STATE] hidden");
|
||||
ref.read(appStateProvider.notifier).handleAppHidden();
|
||||
break;
|
||||
}
|
||||
@@ -200,14 +201,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
initApp().then((_) => debugPrint("App Init Completed"));
|
||||
initApp().then((_) => dPrint(() => "App Init Completed"));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// needs to be delayed so that EasyLocalization is working
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
ref.read(backgroundServiceProvider).disableService();
|
||||
ref.read(driftBackgroundUploadFgService).enable();
|
||||
ref.read(backgroundWorkerFgServiceProvider).enable();
|
||||
} else {
|
||||
ref.read(driftBackgroundUploadFgService).disable();
|
||||
ref.read(backgroundWorkerFgServiceProvider).disable();
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
}
|
||||
});
|
||||
@@ -239,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
|
||||
routerConfig: router.config(
|
||||
deepLinkBuilder: _deepLinkBuilder,
|
||||
navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()],
|
||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -171,7 +171,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
album.activityEnabled = value;
|
||||
}
|
||||
},
|
||||
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"comments_and_likes",
|
||||
|
||||
@@ -205,9 +205,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
buildBackgroundBackupInfo() {
|
||||
return const ListTile(
|
||||
leading: Icon(Icons.info_outline_rounded),
|
||||
title: Text("Background backup is currently running, cannot start manual backup"),
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.info_outline_rounded),
|
||||
title: Text('background_backup_running_error'.tr()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,19 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -25,6 +29,8 @@ class DriftBackupPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
Timer? _countPoller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -33,22 +39,40 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
||||
_startCountPolling();
|
||||
}
|
||||
|
||||
ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
}
|
||||
|
||||
Future<void> startBackup() async {
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
void _startCountPolling() {
|
||||
_countPoller?.cancel();
|
||||
_countPoller = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(backgroundSyncProvider).syncRemote();
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
await ref.read(driftBackupProvider.notifier).cancel();
|
||||
void _stopCountPolling() {
|
||||
_countPoller?.cancel();
|
||||
_countPoller = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopCountPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -58,6 +82,26 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||
.toList();
|
||||
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
|
||||
Future<void> startBackup() async {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await backgroundManager.syncRemote();
|
||||
await backupNotifier.getBackupStatus(currentUser.id);
|
||||
await backupNotifier.startBackup(currentUser.id);
|
||||
_startCountPolling();
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
await backupNotifier.cancel();
|
||||
_stopCountPolling();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
@@ -249,7 +250,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const CircularProgressIndicator(strokeWidth: 4),
|
||||
Text("Creating linked albums...", style: context.textTheme.labelLarge),
|
||||
Text('creating_linked_albums'.tr(), style: context.textTheme.labelLarge),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -58,8 +59,10 @@ class DriftBackupAssetDetailPage extends ConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) =>
|
||||
Text('Error: $error', style: TextStyle(color: context.colorScheme.error)),
|
||||
error: (error, stackTrace) => Text(
|
||||
'error_saving_image'.tr(args: [error.toString()]),
|
||||
style: TextStyle(color: context.colorScheme.error),
|
||||
),
|
||||
loading: () => const SizedBox(height: 16, width: 16, child: CircularProgressIndicator.adaptive()),
|
||||
),
|
||||
],
|
||||
@@ -83,7 +86,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
error: (Object error, StackTrace stackTrace) {
|
||||
return Center(child: Text('Error: $error'));
|
||||
return Center(child: Text('error_saving_image'.tr(args: [error.toString()])));
|
||||
},
|
||||
loading: () {
|
||||
return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive()));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -8,7 +9,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogPage extends HookConsumerWidget {
|
||||
@@ -49,7 +49,7 @@ class AppLogPage extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Logs", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
|
||||
title: Text('logs'.tr(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
|
||||
scrolledUnderElevation: 1,
|
||||
elevation: 2,
|
||||
actions: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -36,7 +37,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
context.scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
"copied_to_clipboard".tr(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
),
|
||||
@@ -97,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Log Detail")),
|
||||
appBar: AppBar(title: Text("log_detail_title".tr())),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -13,6 +15,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
@@ -41,49 +44,13 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
|
||||
Future<void> _handleMigration() async {
|
||||
try {
|
||||
if (widget.switchingToBeta) {
|
||||
final assetNotifier = ref.read(assetProvider.notifier);
|
||||
if (assetNotifier.mounted) {
|
||||
assetNotifier.dispose();
|
||||
}
|
||||
final albumNotifier = ref.read(albumProvider.notifier);
|
||||
if (albumNotifier.mounted) {
|
||||
albumNotifier.dispose();
|
||||
}
|
||||
|
||||
// Cancel uploads
|
||||
await Store.put(StoreKey.backgroundBackup, false);
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
||||
ref.read(backupProvider.notifier).setAutoBackup(false);
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
// Start listening to new websocket events
|
||||
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||
|
||||
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
|
||||
if (permission.isGranted) {
|
||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).disableService();
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
await ref.read(driftBackgroundUploadFgService).disable();
|
||||
}
|
||||
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await _performMigrationLogic().timeout(
|
||||
const Duration(minutes: 3),
|
||||
onTimeout: () async {
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
},
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -101,6 +68,54 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performMigrationLogic() async {
|
||||
if (widget.switchingToBeta) {
|
||||
final assetNotifier = ref.read(assetProvider.notifier);
|
||||
if (assetNotifier.mounted) {
|
||||
assetNotifier.dispose();
|
||||
}
|
||||
final albumNotifier = ref.read(albumProvider.notifier);
|
||||
if (albumNotifier.mounted) {
|
||||
albumNotifier.dispose();
|
||||
}
|
||||
|
||||
// Cancel uploads
|
||||
await Store.put(StoreKey.backgroundBackup, false);
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
||||
ref.read(backupProvider.notifier).setAutoBackup(false);
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
// Start listening to new websocket events
|
||||
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||
|
||||
await ref.read(driftProvider).reset();
|
||||
await Store.put(StoreKey.shouldResetSync, true);
|
||||
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
|
||||
if (permission.isGranted) {
|
||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).disableService();
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
await ref.read(backgroundWorkerFgServiceProvider).disable();
|
||||
}
|
||||
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -11,8 +11,7 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
|
||||
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||
@@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
|
||||
import 'package:immich_mobile/widgets/settings/settings_card.dart';
|
||||
|
||||
enum SettingSection {
|
||||
beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"),
|
||||
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
|
||||
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
|
||||
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
|
||||
@@ -28,14 +26,14 @@ enum SettingSection {
|
||||
networking('networking_settings', Icons.wifi, "networking_subtitle"),
|
||||
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
|
||||
preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"),
|
||||
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle");
|
||||
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"),
|
||||
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle");
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
|
||||
Widget get widget => switch (this) {
|
||||
SettingSection.beta => const _BetaLandscapeToggle(),
|
||||
SettingSection.advanced => const AdvancedSettings(),
|
||||
SettingSection.assetViewer => const AssetViewerSettings(),
|
||||
SettingSection.backup =>
|
||||
@@ -45,6 +43,7 @@ enum SettingSection {
|
||||
SettingSection.notifications => const NotificationSetting(),
|
||||
SettingSection.preferences => const PreferenceSetting(),
|
||||
SettingSection.timeline => const AssetListSettings(),
|
||||
SettingSection.beta => const SyncStatusAndActions(),
|
||||
};
|
||||
|
||||
const SettingSection(this.title, this.icon, this.subtitle);
|
||||
@@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget {
|
||||
context.locale;
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: const Text('settings').tr()),
|
||||
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
|
||||
body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,13 +71,12 @@ class _MobileLayout extends StatelessWidget {
|
||||
.expand(
|
||||
(setting) => setting == SettingSection.beta
|
||||
? [
|
||||
const BetaTimelineListTile(),
|
||||
if (Store.isBetaTimelineEnabled)
|
||||
SettingsCard(
|
||||
icon: Icons.sync_outlined,
|
||||
title: 'beta_sync'.tr(),
|
||||
subtitle: 'beta_sync_subtitle'.tr(),
|
||||
settingRoute: const BetaSyncSettingsRoute(),
|
||||
title: 'sync_status'.tr(),
|
||||
subtitle: 'sync_status_subtitle'.tr(),
|
||||
settingRoute: const SyncStatusRoute(),
|
||||
),
|
||||
]
|
||||
: [
|
||||
@@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget {
|
||||
.toList();
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
|
||||
padding: const EdgeInsets.only(top: 10.0, bottom: 16),
|
||||
children: [...settings],
|
||||
);
|
||||
}
|
||||
@@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _BetaLandscapeToggle extends HookWidget {
|
||||
const _BetaLandscapeToggle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 100, child: BetaTimelineListTile()),
|
||||
if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SettingsSubPage extends StatelessWidget {
|
||||
const SettingsSubPage(this.section, {super.key});
|
||||
@@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
context.locale;
|
||||
return Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||
body: section.widget,
|
||||
return SafeArea(
|
||||
bottom: true,
|
||||
top: false,
|
||||
right: true,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
|
||||
body: section.widget,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
@@ -29,19 +27,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||
|
||||
lockManager.requestHolderToClose();
|
||||
lockManager
|
||||
.acquireLock()
|
||||
.timeout(const Duration(seconds: 5))
|
||||
.whenComplete(
|
||||
() => ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession()),
|
||||
);
|
||||
ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession());
|
||||
}
|
||||
|
||||
void logConnectionInfo(String? endpoint) {
|
||||
@@ -60,32 +50,56 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||
final wsProvider = ref.read(websocketProvider.notifier);
|
||||
unawaited(
|
||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||
(a) {
|
||||
try {
|
||||
wsProvider.connect();
|
||||
infoProvider.getServerInfo();
|
||||
} catch (e) {
|
||||
log.severe('Failed establishing connection to the server: $e');
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
|
||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||
(_) async {
|
||||
try {
|
||||
wsProvider.connect();
|
||||
infoProvider.getServerInfo();
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]);
|
||||
await Future.wait([
|
||||
backgroundManager.hashAssets().then((_) {
|
||||
_resumeBackup(backupProvider);
|
||||
}),
|
||||
_resumeBackup(backupProvider),
|
||||
]);
|
||||
|
||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
||||
await backgroundManager.syncLinkedAlbum();
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (exception) => {
|
||||
log.severe('Failed to update auth info with access token: $accessToken'),
|
||||
ref.read(authProvider.notifier).logout(),
|
||||
context.replaceRoute(const LoginRoute()),
|
||||
},
|
||||
),
|
||||
} catch (e) {
|
||||
log.severe('Failed establishing connection to the server: $e');
|
||||
}
|
||||
},
|
||||
onError: (exception) => {
|
||||
log.severe('Failed to update auth info with access token: $accessToken'),
|
||||
ref.read(authProvider.notifier).logout(),
|
||||
context.replaceRoute(const LoginRoute()),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
log.severe('Missing crucial offline login info - Logging out completely');
|
||||
unawaited(ref.read(authProvider.notifier).logout());
|
||||
unawaited(context.replaceRoute(const LoginRoute()));
|
||||
ref.read(authProvider.notifier).logout();
|
||||
context.replaceRoute(const LoginRoute());
|
||||
return;
|
||||
}
|
||||
|
||||
// clean install - change the default of the flag
|
||||
// current install not using beta timeline
|
||||
if (context.router.current.name == SplashScreenRoute.name) {
|
||||
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
||||
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
||||
if (needBetaMigration) {
|
||||
await Store.put(StoreKey.needBetaMigration, false);
|
||||
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]);
|
||||
return;
|
||||
}
|
||||
|
||||
context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute());
|
||||
}
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
@@ -95,7 +109,18 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission;
|
||||
if (hasPermission) {
|
||||
// Resume backup (if enable) then navigate
|
||||
await ref.watch(backupProvider.notifier).resumeBackup();
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
|
||||
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
if (currentUser != null) {
|
||||
notifier.handleBackupResume(currentUser.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
@@ -17,11 +15,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
|
||||
@RoutePage()
|
||||
class TabShellPage extends ConsumerStatefulWidget {
|
||||
@@ -32,28 +26,6 @@ class TabShellPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
|
||||
final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
await runNewSync(ref, full: true).then((_) async {
|
||||
if (isEnableBackup) {
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
|
||||
@@ -134,7 +134,7 @@ class _SharedToPartnerList extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text("Error loading partners: $error")),
|
||||
error: (error, stack) => Center(child: Text('error_loading_partners'.tr(args: [error.toString()]))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stask) => const Text('Error getting places'),
|
||||
error: (error, stask) => Text('error_getting_places'.tr()),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -112,7 +112,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
return SwitchListTile.adaptive(
|
||||
value: showMetadata.value,
|
||||
onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null,
|
||||
activeColor: colorScheme.primary,
|
||||
activeThumbColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text("show_metadata", style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(),
|
||||
);
|
||||
@@ -122,7 +122,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
return SwitchListTile.adaptive(
|
||||
value: allowDownload.value,
|
||||
onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null,
|
||||
activeColor: colorScheme.primary,
|
||||
activeThumbColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"allow_public_user_to_download",
|
||||
@@ -135,7 +135,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
return SwitchListTile.adaptive(
|
||||
value: allowUpload.value,
|
||||
onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null,
|
||||
activeColor: colorScheme.primary,
|
||||
activeThumbColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"allow_public_user_to_upload",
|
||||
@@ -148,7 +148,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
return SwitchListTile.adaptive(
|
||||
value: editExpiry.value,
|
||||
onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null,
|
||||
activeColor: colorScheme.primary,
|
||||
activeThumbColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"change_expiration_time",
|
||||
|
||||
@@ -435,7 +435,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
tooltip: 'Show text search menu',
|
||||
tooltip: 'show_text_search_menu'.tr(),
|
||||
);
|
||||
},
|
||||
menuChildren: [
|
||||
|
||||
+5
-5
@@ -1,25 +1,25 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BetaSyncSettingsPage extends StatelessWidget {
|
||||
const BetaSyncSettingsPage({super.key});
|
||||
class SyncStatusPage extends StatelessWidget {
|
||||
const SyncStatusPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
title: const Text("beta_sync").t(context: context),
|
||||
title: const Text("sync_status").t(context: context),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.maybePop(true),
|
||||
splashRadius: 24,
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
body: const BetaSyncSettings(),
|
||||
body: const SyncStatusAndActions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -142,6 +142,29 @@ class BackgroundWorkerBgHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showNotification(String title, String content) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[title, content]);
|
||||
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<void> close() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), 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".',
|
||||
);
|
||||
}
|
||||
|
||||
enum NetworkCapability { cellular, wifi, vpn, unmetered }
|
||||
|
||||
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 NetworkCapability) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : NetworkCapability.values[value];
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectivityApi {
|
||||
/// Constructor for [ConnectivityApi]. 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.
|
||||
ConnectivityApi({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<List<NetworkCapability>> getCapabilities() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$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 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 List<Object?>?)!.cast<NetworkCapability>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -135,7 +136,7 @@ class FeatInDevPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Features in Development'), centerTitle: true),
|
||||
appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true),
|
||||
body: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
|
||||
@@ -15,6 +15,7 @@ class MainTimelinePage extends ConsumerWidget {
|
||||
return Timeline(
|
||||
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
|
||||
topSliverWidgetHeight: hasMemories ? 200 : 0,
|
||||
showStorageIndicator: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -55,7 +56,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Local Media Summary')),
|
||||
appBar: AppBar(title: Text('local_media_summary'.tr())),
|
||||
body: Consumer(
|
||||
builder: (ctx, ref, __) {
|
||||
final db = ref.watch(driftProvider);
|
||||
@@ -78,7 +79,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text("Album summary", style: ctx.textTheme.titleMedium),
|
||||
child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -135,7 +136,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Remote Media Summary')),
|
||||
appBar: AppBar(title: Text('remote_media_summary'.tr())),
|
||||
body: Consumer(
|
||||
builder: (ctx, ref, __) {
|
||||
final db = ref.watch(driftProvider);
|
||||
@@ -158,7 +159,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text("Album summary", style: ctx.textTheme.titleMedium),
|
||||
child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -210,7 +210,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
||||
activityEnabled.value = value;
|
||||
await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value);
|
||||
},
|
||||
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"comments_and_likes",
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssetTroubleshootPage extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const AssetTroubleshootPage({super.key, required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('asset_troubleshoot'.tr())),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _AssetDetailsView(asset: asset),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetDetailsView extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const _AssetDetailsView({required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_AssetPropertiesSection(asset: asset),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'matching_assets'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (asset.checksum != null) ...[
|
||||
_LocalAssetsSection(asset: asset),
|
||||
const SizedBox(height: 16),
|
||||
_RemoteAssetSection(asset: asset),
|
||||
] else ...[
|
||||
_PropertySectionCard(
|
||||
title: 'Local Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'no_checksum_local'.tr())],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_PropertySectionCard(
|
||||
title: 'Remote Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'no_checksum_remote'.tr())],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetPropertiesSection extends ConsumerStatefulWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const _AssetPropertiesSection({required this.asset});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _AssetPropertiesSectionState();
|
||||
}
|
||||
|
||||
class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection> {
|
||||
List<_PropertyItem> properties = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_buildAssetProperties(widget.asset).whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = _getAssetTypeTitle(widget.asset);
|
||||
|
||||
return _PropertySectionCard(title: title, properties: properties);
|
||||
}
|
||||
|
||||
Future<void> _buildAssetProperties(BaseAsset asset) async {
|
||||
_addCommonProperties();
|
||||
|
||||
if (asset is LocalAsset) {
|
||||
await _addLocalAssetProperties(asset);
|
||||
} else if (asset is RemoteAsset) {
|
||||
await _addRemoteAssetProperties(asset);
|
||||
}
|
||||
}
|
||||
|
||||
void _addCommonProperties() {
|
||||
final asset = widget.asset;
|
||||
properties.addAll([
|
||||
_PropertyItem(label: 'Name', value: asset.name),
|
||||
_PropertyItem(label: 'Checksum', value: asset.checksum),
|
||||
_PropertyItem(label: 'Type', value: asset.type.toString()),
|
||||
_PropertyItem(label: 'Created At', value: asset.createdAt.toString()),
|
||||
_PropertyItem(label: 'Updated At', value: asset.updatedAt.toString()),
|
||||
_PropertyItem(label: 'Width', value: asset.width?.toString()),
|
||||
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
||||
_PropertyItem(
|
||||
label: 'Duration',
|
||||
value: asset.durationInSeconds != null ? '${asset.durationInSeconds} seconds' : null,
|
||||
),
|
||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _addLocalAssetProperties(LocalAsset asset) async {
|
||||
properties.insertAll(0, [
|
||||
_PropertyItem(label: 'Local ID', value: asset.id),
|
||||
_PropertyItem(label: 'Remote ID', value: asset.remoteId),
|
||||
]);
|
||||
|
||||
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
|
||||
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
|
||||
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
|
||||
}
|
||||
|
||||
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
|
||||
properties.insertAll(0, [
|
||||
_PropertyItem(label: 'Remote ID', value: asset.id),
|
||||
_PropertyItem(label: 'Local ID', value: asset.localId),
|
||||
_PropertyItem(label: 'Owner ID', value: asset.ownerId),
|
||||
]);
|
||||
|
||||
final additionalProps = <_PropertyItem>[
|
||||
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
||||
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
||||
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
||||
];
|
||||
|
||||
properties.insertAll(4, additionalProps);
|
||||
|
||||
final exif = await ref.read(assetServiceProvider).getExif(asset);
|
||||
if (exif != null) {
|
||||
_addExifProperties(exif);
|
||||
} else {
|
||||
properties.add(const _PropertyItem(label: 'EXIF', value: null));
|
||||
}
|
||||
}
|
||||
|
||||
void _addExifProperties(ExifInfo exif) {
|
||||
properties.addAll([
|
||||
_PropertyItem(
|
||||
label: 'File Size',
|
||||
value: exif.fileSize != null ? '${(exif.fileSize! / 1024 / 1024).toStringAsFixed(2)} MB' : null,
|
||||
),
|
||||
_PropertyItem(label: 'Description', value: exif.description),
|
||||
_PropertyItem(label: 'EXIF Width', value: exif.width?.toString()),
|
||||
_PropertyItem(label: 'EXIF Height', value: exif.height?.toString()),
|
||||
_PropertyItem(label: 'Date Taken', value: exif.dateTimeOriginal?.toString()),
|
||||
_PropertyItem(label: 'Time Zone', value: exif.timeZone),
|
||||
_PropertyItem(label: 'Camera Make', value: exif.make),
|
||||
_PropertyItem(label: 'Camera Model', value: exif.model),
|
||||
_PropertyItem(label: 'Lens', value: exif.lens),
|
||||
_PropertyItem(label: 'F-Number', value: exif.f != null ? 'f/${exif.fNumber}' : null),
|
||||
_PropertyItem(label: 'Focal Length', value: exif.mm != null ? '${exif.focalLength}mm' : null),
|
||||
_PropertyItem(label: 'ISO', value: exif.iso?.toString()),
|
||||
_PropertyItem(label: 'Exposure Time', value: exif.exposureTime.isNotEmpty ? exif.exposureTime : null),
|
||||
_PropertyItem(
|
||||
label: 'GPS Coordinates',
|
||||
value: exif.hasCoordinates ? '${exif.latitude}, ${exif.longitude}' : null,
|
||||
),
|
||||
_PropertyItem(
|
||||
label: 'Location',
|
||||
value: [exif.city, exif.state, exif.country].where((e) => e != null && e.isNotEmpty).join(', '),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
String _getAssetTypeTitle(BaseAsset asset) {
|
||||
if (asset is LocalAsset) return 'Local Asset';
|
||||
if (asset is RemoteAsset) return 'Remote Asset';
|
||||
return 'Base Asset';
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalAssetsSection extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const _LocalAssetsSection({required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
|
||||
return FutureBuilder<List<LocalAsset?>>(
|
||||
future: assetService.getLocalAssetsByChecksum(asset.checksum!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const _PropertySectionCard(
|
||||
title: 'Local Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return _PropertySectionCard(
|
||||
title: 'Local Assets',
|
||||
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
|
||||
);
|
||||
}
|
||||
|
||||
final localAssets = snapshot.data?.cast<LocalAsset>() ?? [];
|
||||
if (asset is LocalAsset) {
|
||||
localAssets.removeWhere((a) => a.id == (asset as LocalAsset).id);
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
return _PropertySectionCard(
|
||||
title: 'Local Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'no_local_assets_found'.tr())],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (localAssets.length > 1)
|
||||
_PropertySectionCard(
|
||||
title: 'Local Assets Summary',
|
||||
properties: [_PropertyItem(label: 'Total Count', value: localAssets.length.toString())],
|
||||
),
|
||||
...localAssets.map((localAsset) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: _AssetPropertiesSection(asset: localAsset),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RemoteAssetSection extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
|
||||
const _RemoteAssetSection({required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
|
||||
if (asset is RemoteAsset) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FutureBuilder<RemoteAsset?>(
|
||||
future: assetService.getRemoteAssetByChecksum(asset.checksum!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const _PropertySectionCard(
|
||||
title: 'Remote Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return _PropertySectionCard(
|
||||
title: 'Remote Assets',
|
||||
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
|
||||
);
|
||||
}
|
||||
|
||||
final remoteAsset = snapshot.data;
|
||||
|
||||
if (remoteAsset == null) {
|
||||
return _PropertySectionCard(
|
||||
title: 'Remote Assets',
|
||||
properties: [_PropertyItem(label: 'Status', value: 'no_remote_assets_found'.tr())],
|
||||
);
|
||||
}
|
||||
|
||||
return _AssetPropertiesSection(asset: remoteAsset);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertySectionCard extends StatelessWidget {
|
||||
final String title;
|
||||
final List<_PropertyItem> properties;
|
||||
|
||||
const _PropertySectionCard({required this.title, required this.properties});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
...properties,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertyItem extends StatelessWidget {
|
||||
final String label;
|
||||
final String? value;
|
||||
|
||||
const _PropertyItem({required this.label, this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ?? 'not_available'.tr(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
@@ -302,7 +303,9 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
||||
}).toList();
|
||||
},
|
||||
error: (error, _) {
|
||||
return [Center(child: Text('Error: $error'))];
|
||||
return [
|
||||
Center(child: Text('error_saving_image'.tr(args: [error.toString()]))),
|
||||
];
|
||||
},
|
||||
loading: () {
|
||||
return [const Center(child: CircularProgressIndicator())];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
@@ -46,9 +47,9 @@ class _AlbumList extends ConsumerWidget {
|
||||
),
|
||||
data: (albums) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
|
||||
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('no_albums_yet'.tr())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftPartnerDetailPage extends StatelessWidget {
|
||||
@@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
|
||||
_inTimeline = !_inTimeline;
|
||||
});
|
||||
} catch (error, stack) {
|
||||
debugPrint("Failed to toggle in timeline: $error $stack");
|
||||
dPrint(() => "Failed to toggle in timeline: $error $stack");
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
toastType: ToastType.error,
|
||||
|
||||
@@ -3,9 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
@@ -26,11 +25,9 @@ final driftUsersProvider = FutureProvider.autoDispose<List<UserDto>>((ref) async
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
email: entity.email,
|
||||
isAdmin: entity.isAdmin,
|
||||
updatedAt: entity.updatedAt,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
avatarColor: AvatarColor.primary,
|
||||
avatarColor: entity.avatarColor,
|
||||
memoryEnabled: true,
|
||||
inTimeline: true,
|
||||
profileChangedAt: entity.profileChangedAt,
|
||||
|
||||
@@ -26,6 +26,7 @@ class LocalTimelinePage extends StatelessWidget {
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||
bottomSheet: const LocalAlbumBottomSheet(),
|
||||
showStorageIndicator: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
tooltip: 'Show text search menu',
|
||||
tooltip: 'show_text_search_menu'.tr(),
|
||||
);
|
||||
},
|
||||
menuChildren: [
|
||||
@@ -633,6 +633,7 @@ class _SearchResultGrid extends ConsumerWidget {
|
||||
groupBy: GroupAssetsBy.none,
|
||||
appBar: null,
|
||||
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||
withScrubber: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
class AdvancedInfoActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
|
||||
const AdvancedInfoActionButton({super.key, required this.source});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(actionProvider.notifier).troubleshoot(source, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.help_outline_rounded,
|
||||
label: "troubleshoot".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -58,7 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
|
||||
label: "like".t(context: context),
|
||||
menuItem: menuItem,
|
||||
),
|
||||
error: (error, stack) => Text("Error: $error"),
|
||||
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
final menuController = MenuController();
|
||||
final searchFocusNode = FocusNode();
|
||||
List<RemoteAlbum> sortedAlbums = [];
|
||||
List<RemoteAlbum> shownAlbums = [];
|
||||
@@ -157,34 +158,45 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
await sortAlbums();
|
||||
});
|
||||
|
||||
return MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filter.mode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filter.mode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
|
||||
isGrid
|
||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
menuController.close();
|
||||
},
|
||||
child: MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filter.mode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filter.mode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(
|
||||
isGrid: isGrid,
|
||||
onToggleViewMode: toggleViewMode,
|
||||
onSortChanged: changeSort,
|
||||
controller: menuController,
|
||||
),
|
||||
isGrid
|
||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton(this.onSortChanged);
|
||||
const _SortButton(this.onSortChanged, {this.controller});
|
||||
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
final MenuController? controller;
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
@@ -220,6 +232,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuAnchor(
|
||||
controller: widget.controller,
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
@@ -449,10 +462,16 @@ class _QuickFilterButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _QuickSortAndViewMode extends StatelessWidget {
|
||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});
|
||||
const _QuickSortAndViewMode({
|
||||
required this.isGrid,
|
||||
required this.onToggleViewMode,
|
||||
required this.onSortChanged,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
final bool isGrid;
|
||||
final VoidCallback onToggleViewMode;
|
||||
final MenuController? controller;
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
@@ -463,7 +482,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_SortButton(onSortChanged),
|
||||
_SortButton(onSortChanged, controller: controller),
|
||||
IconButton(
|
||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||
onPressed: onToggleViewMode,
|
||||
@@ -485,9 +504,9 @@ class _AlbumList extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
|
||||
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -580,9 +599,9 @@ class _AlbumGrid extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
|
||||
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||
@@ -30,7 +31,6 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssetViewerPage extends StatelessWidget {
|
||||
@@ -53,10 +53,9 @@ class AssetViewerPage extends StatelessWidget {
|
||||
|
||||
class AssetViewer extends ConsumerStatefulWidget {
|
||||
final int initialIndex;
|
||||
final Platform? platform;
|
||||
final int? heroOffset;
|
||||
|
||||
const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset});
|
||||
const AssetViewer({super.key, required this.initialIndex, this.heroOffset});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _AssetViewerState();
|
||||
@@ -86,7 +85,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
PhotoViewControllerBase? viewController;
|
||||
StreamSubscription? reloadSubscription;
|
||||
|
||||
late Platform platform;
|
||||
late final int heroOffset;
|
||||
late PhotoViewControllerValue initialPhotoViewState;
|
||||
bool? hasDraggedDown;
|
||||
@@ -114,7 +112,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
super.initState();
|
||||
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
|
||||
pageController = PageController(initialPage: widget.initialIndex);
|
||||
platform = widget.platform ?? const LocalPlatform();
|
||||
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||
bottomSheetController = DraggableScrollableController();
|
||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||
@@ -638,7 +635,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: _placeholderBuilder,
|
||||
pageController: pageController,
|
||||
scrollPhysics: platform.isIOS
|
||||
scrollPhysics: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||
itemCount: totalAssets,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
|
||||
@@ -14,6 +15,7 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -41,6 +43,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
@@ -49,6 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
isTrashEnabled: isTrashEnable,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
@@ -122,6 +126,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
|
||||
}
|
||||
|
||||
Future<void> _editDateTime(BuildContext context, WidgetRef ref) async {
|
||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
@@ -132,10 +140,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||
|
||||
Future<void> editDateTime() async {
|
||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
return SliverList.list(
|
||||
children: [
|
||||
// Asset Date and Time
|
||||
@@ -143,7 +147,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
title: _getDateTime(context, asset),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote ? () async => await editDateTime() : null,
|
||||
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
||||
const SheetPeopleDetails(),
|
||||
|
||||
@@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
||||
final previousRouteName = ref.watch(previousRouteNameProvider);
|
||||
final tabRoute = ref.watch(tabProvider);
|
||||
final showViewInTimelineButton =
|
||||
previousRouteName != TabShellRoute.name &&
|
||||
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
|
||||
previousRouteName != AssetViewerRoute.name &&
|
||||
previousRouteName != null;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
@@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
@@ -43,7 +43,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
@@ -21,6 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -51,6 +54,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
@@ -88,19 +92,22 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
|
||||
const AdvancedInfoActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
const DeleteActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||
@@ -100,7 +100,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -98,7 +98,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
|
||||
final operation = cachedOperation;
|
||||
if (operation != null) {
|
||||
this.cachedOperation = null;
|
||||
cachedOperation = null;
|
||||
operation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:scroll_date_picker/scroll_date_picker.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
|
||||
final DriftPerson person;
|
||||
@@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
|
||||
context.pop<DateTime>(_selectedDate);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('Error updating birthday: $error');
|
||||
dPrint(() => 'Error updating birthday: $error');
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
|
||||
final DriftPerson person;
|
||||
@@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
|
||||
context.pop<String>(newName);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('Error updating name: $error');
|
||||
dPrint(() => 'Error updating name: $error');
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
||||
@@ -3,14 +3,15 @@ import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
/// for quick navigation of the BoxScrollView.
|
||||
@@ -45,7 +46,7 @@ class Scrubber extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
List<_Segment> _buildSegments({required List<Segment> layoutSegments, required double timelineHeight}) {
|
||||
const double offsetThreshold = 20.0;
|
||||
const double offsetThreshold = 40.0;
|
||||
|
||||
final segments = <_Segment>[];
|
||||
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
||||
@@ -79,6 +80,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
double _thumbTopOffset = 0.0;
|
||||
bool _isDragging = false;
|
||||
List<_Segment> _segments = [];
|
||||
int _monthCount = 0;
|
||||
|
||||
late AnimationController _thumbAnimationController;
|
||||
Timer? _fadeOutTimer;
|
||||
@@ -105,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
|
||||
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
|
||||
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
|
||||
_monthCount = getMonthCount();
|
||||
|
||||
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
|
||||
}
|
||||
@@ -121,6 +124,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
|
||||
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
|
||||
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
|
||||
_monthCount = getMonthCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +144,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
});
|
||||
}
|
||||
|
||||
int getMonthCount() {
|
||||
return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length;
|
||||
}
|
||||
|
||||
bool _onScrollNotification(ScrollNotification notification) {
|
||||
if (_isDragging) {
|
||||
// If the user is dragging the thumb, we don't want to update the position
|
||||
@@ -169,7 +177,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
}
|
||||
|
||||
void _onDragStart(DragStartDetails _) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_labelAnimationController.forward();
|
||||
@@ -191,13 +202,22 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
|
||||
|
||||
if (nearestMonthSegment != null) {
|
||||
_snapToSegment(nearestMonthSegment);
|
||||
final label = nearestMonthSegment.scrollLabel;
|
||||
if (_lastLabel != label) {
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
_lastLabel = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
|
||||
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
|
||||
setState(() {
|
||||
_thumbTopOffset = dragPosition;
|
||||
_scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent);
|
||||
});
|
||||
} else if (nearestMonthSegment != null) {
|
||||
_snapToSegment(nearestMonthSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the drag position relative to the scrubber area
|
||||
@@ -370,7 +390,7 @@ class _SegmentWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
margin: const EdgeInsets.only(right: 36.0),
|
||||
child: Material(
|
||||
color: context.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user