mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/search-filter-album/web
This commit is contained in:
@@ -106,6 +106,7 @@ custom_lint:
|
||||
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
||||
- lib/domain/services/search.service.dart
|
||||
|
||||
# refactor
|
||||
- lib/models/map/map_marker.model.dart
|
||||
|
||||
@@ -3,6 +3,8 @@ plugins {
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version
|
||||
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
@@ -45,6 +47,10 @@ android {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "app.alextran.immich"
|
||||
minSdkVersion 26
|
||||
@@ -105,6 +111,8 @@ dependencies {
|
||||
def guava_version = '33.3.1-android'
|
||||
def glide_version = '4.16.0'
|
||||
def serialization_version = '1.8.1'
|
||||
def compose_version = '1.1.1'
|
||||
def gson_version = '2.10.1'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
@@ -116,6 +124,17 @@ dependencies {
|
||||
|
||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
||||
|
||||
//Glance Widget
|
||||
implementation "androidx.glance:glance-appwidget:$compose_version"
|
||||
implementation "com.google.code.gson:gson:$gson_version"
|
||||
|
||||
// Glance Configure
|
||||
implementation "androidx.activity:activity-compose:1.8.2"
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
implementation "androidx.compose.material3:material3:1.2.1"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||
}
|
||||
|
||||
// This is uncommented in F-Droid build script
|
||||
|
||||
+8
-1
@@ -25,8 +25,15 @@
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# TypeToken preventions
|
||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||
-keep class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
# Keep all widget model classes and their fields for Gson
|
||||
-keep class app.alextran.immich.widget.model.** { *; }
|
||||
@@ -141,6 +141,41 @@
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
|
||||
|
||||
<!-- Widgets -->
|
||||
<receiver
|
||||
android:name=".widget.RandomReceiver"
|
||||
android:exported="true"
|
||||
android:label="@string/random_widget_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/random_widget" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.MemoryReceiver"
|
||||
android:exported="true"
|
||||
android:label="@string/memory_widget_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/memory_widget" />
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".widget.configure.RandomConfigure"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -154,4 +189,4 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -83,6 +83,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
||||
|
||||
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||
runDart()
|
||||
|
||||
}
|
||||
|
||||
return resolvableFuture
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.io.File
|
||||
|
||||
fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||
options.inJustDecodeBounds = false
|
||||
|
||||
return BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
}
|
||||
|
||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||
var inSampleSize = 1
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
|
||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.*
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import app.alextran.immich.widget.model.*
|
||||
import java.time.LocalDate
|
||||
|
||||
class ImageDownloadWorker(
|
||||
private val context: Context,
|
||||
workerParameters: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
||||
|
||||
private fun buildConstraints(): Constraints {
|
||||
return Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
|
||||
return Data.Builder()
|
||||
.putString(kWorkerWidgetType, widgetType.toString())
|
||||
.putInt(kWorkerWidgetID, appWidgetId)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||
val manager = WorkManager.getInstance(context)
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
||||
20, TimeUnit.MINUTES
|
||||
)
|
||||
.setConstraints(buildConstraints())
|
||||
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||
.addTag(appWidgetId.toString())
|
||||
.build()
|
||||
|
||||
manager.enqueueUniquePeriodicWork(
|
||||
"$uniqueWorkName-$appWidgetId",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||
val manager = WorkManager.getInstance(context)
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
|
||||
.setConstraints(buildConstraints())
|
||||
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||
.addTag(appWidgetId.toString())
|
||||
.build()
|
||||
|
||||
manager.enqueueUniqueWork(
|
||||
"$uniqueWorkName-$appWidgetId",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun cancel(context: Context, appWidgetId: Int) {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId")
|
||||
|
||||
// delete cached image
|
||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
||||
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||
val currentImgUUID = widgetConfig[kImageUUID]
|
||||
|
||||
if (!currentImgUUID.isNullOrEmpty()) {
|
||||
val file = File(context.cacheDir, imageFilename(currentImgUUID))
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||
val currentImgUUID = widgetConfig[kImageUUID]
|
||||
|
||||
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||
|
||||
// clear any image caches and go to "login" state if no credentials
|
||||
if (serverConfig == null) {
|
||||
if (!currentImgUUID.isNullOrEmpty()) {
|
||||
deleteImage(currentImgUUID)
|
||||
updateWidget(
|
||||
glanceId,
|
||||
"",
|
||||
"",
|
||||
"immich://",
|
||||
WidgetState.LOG_IN
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
// fetch new image
|
||||
val entry = when (widgetType) {
|
||||
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
|
||||
WidgetType.MEMORIES -> fetchMemory(serverConfig)
|
||||
}
|
||||
|
||||
// clear current image if it exists
|
||||
if (!currentImgUUID.isNullOrEmpty()) {
|
||||
deleteImage(currentImgUUID)
|
||||
}
|
||||
|
||||
// save a new image
|
||||
val imgUUID = UUID.randomUUID().toString()
|
||||
saveImage(entry.image, imgUUID)
|
||||
|
||||
// trigger the update routine with new image uuid
|
||||
updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
|
||||
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(uniqueWorkName, "Error while loading image", e)
|
||||
if (runAttemptCount < 10) {
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWidget(
|
||||
glanceId: GlanceId,
|
||||
imageUUID: String,
|
||||
subtitle: String?,
|
||||
deeplink: String?,
|
||||
widgetState: WidgetState = WidgetState.SUCCESS
|
||||
) {
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs[kNow] = System.currentTimeMillis()
|
||||
prefs[kImageUUID] = imageUUID
|
||||
prefs[kWidgetState] = widgetState.toString()
|
||||
prefs[kSubtitleText] = subtitle ?: ""
|
||||
prefs[kDeeplinkURL] = deeplink ?: ""
|
||||
}
|
||||
|
||||
PhotoWidget().update(context,glanceId)
|
||||
}
|
||||
|
||||
private suspend fun fetchRandom(
|
||||
serverConfig: ServerConfig,
|
||||
widgetConfig: Preferences
|
||||
): WidgetEntry {
|
||||
val api = ImmichAPI(serverConfig)
|
||||
|
||||
val filters = SearchFilters()
|
||||
val albumId = widgetConfig[kSelectedAlbum]
|
||||
val showSubtitle = widgetConfig[kShowAlbumName]
|
||||
val albumName = widgetConfig[kSelectedAlbumName]
|
||||
var subtitle: String? = if (showSubtitle == true) albumName else ""
|
||||
|
||||
|
||||
if (albumId == "FAVORITES") {
|
||||
filters.isFavorite = true
|
||||
} else if (albumId != null) {
|
||||
filters.albumIds = listOf(albumId)
|
||||
}
|
||||
|
||||
var randomSearch = api.fetchSearchResults(filters)
|
||||
|
||||
// handle an empty album, fallback to random
|
||||
if (randomSearch.isEmpty() && albumId != null) {
|
||||
randomSearch = api.fetchSearchResults(SearchFilters())
|
||||
subtitle = ""
|
||||
}
|
||||
|
||||
val random = randomSearch.first()
|
||||
val image = api.fetchImage(random)
|
||||
|
||||
return WidgetEntry(
|
||||
image,
|
||||
subtitle,
|
||||
assetDeeplink(random)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchMemory(
|
||||
serverConfig: ServerConfig
|
||||
): WidgetEntry {
|
||||
val api = ImmichAPI(serverConfig)
|
||||
|
||||
val today = LocalDate.now()
|
||||
val memories = api.fetchMemory(today)
|
||||
val asset: Asset
|
||||
var subtitle: String? = null
|
||||
|
||||
if (memories.isNotEmpty()) {
|
||||
// pick a random asset from a random memory
|
||||
val memory = memories.random()
|
||||
asset = memory.assets.random()
|
||||
|
||||
val yearDiff = today.year - memory.data.year
|
||||
subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago"
|
||||
} else {
|
||||
val filters = SearchFilters(size=1)
|
||||
asset = api.fetchSearchResults(filters).first()
|
||||
}
|
||||
|
||||
val image = api.fetchImage(asset)
|
||||
return WidgetEntry(
|
||||
image,
|
||||
subtitle,
|
||||
assetDeeplink(asset)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, imageFilename(uuid))
|
||||
file.delete()
|
||||
}
|
||||
|
||||
private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, imageFilename(uuid))
|
||||
FileOutputStream(file).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import app.alextran.immich.widget.model.*
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class ImmichAPI(cfg: ServerConfig) {
|
||||
|
||||
companion object {
|
||||
fun getServerConfig(context: Context): ServerConfig? {
|
||||
val prefs = HomeWidgetPlugin.getData(context)
|
||||
|
||||
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
||||
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
||||
|
||||
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ServerConfig(
|
||||
serverURL,
|
||||
sessionKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val gson = Gson()
|
||||
private val serverConfig = cfg
|
||||
|
||||
private fun buildRequestURL(endpoint: String, params: List<Pair<String, String>> = emptyList()): URL {
|
||||
val urlString = StringBuilder("${serverConfig.serverEndpoint}$endpoint?sessionKey=${serverConfig.sessionKey}")
|
||||
|
||||
for ((key, value) in params) {
|
||||
urlString.append("&${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}")
|
||||
}
|
||||
|
||||
return URL(urlString.toString())
|
||||
}
|
||||
|
||||
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
||||
val url = buildRequestURL("/search/random")
|
||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
doOutput = true
|
||||
}
|
||||
|
||||
connection.outputStream.use {
|
||||
OutputStreamWriter(it).use { writer ->
|
||||
writer.write(gson.toJson(filters))
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
val type = object : TypeToken<List<Asset>>() {}.type
|
||||
gson.fromJson(response, type)
|
||||
}
|
||||
|
||||
suspend fun fetchMemory(date: LocalDate): List<MemoryResult> = withContext(Dispatchers.IO) {
|
||||
val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
}
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
val type = object : TypeToken<List<MemoryResult>>() {}.type
|
||||
gson.fromJson(response, type)
|
||||
}
|
||||
|
||||
suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) {
|
||||
val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview"))
|
||||
val connection = url.openConnection()
|
||||
val data = connection.getInputStream().readBytes()
|
||||
BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||
?: throw Exception("Invalid image data")
|
||||
}
|
||||
|
||||
suspend fun fetchAlbums(): List<Album> = withContext(Dispatchers.IO) {
|
||||
val url = buildRequestURL("/albums")
|
||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
}
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
val type = object : TypeToken<List<Album>>() {}.type
|
||||
gson.fromJson(response, type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import app.alextran.immich.widget.model.*
|
||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MemoryReceiver : GlanceAppWidgetReceiver() {
|
||||
override val glanceAppWidget = PhotoWidget()
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
|
||||
appWidgetIds.forEach { widgetID ->
|
||||
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
||||
|
||||
// Launch coroutine to setup a single shot if the app requested the update
|
||||
if (fromMainApp) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val provider = ComponentName(context, MemoryReceiver::class.java)
|
||||
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
||||
|
||||
glanceIds.forEach { widgetID ->
|
||||
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.MEMORIES)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
appWidgetIds.forEach { id ->
|
||||
ImageDownloadWorker.cancel(context, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.core.net.toUri
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.glance.appwidget.*
|
||||
import androidx.glance.*
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.layout.*
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import app.alextran.immich.R
|
||||
import app.alextran.immich.widget.model.*
|
||||
import java.io.File
|
||||
|
||||
class PhotoWidget : GlanceAppWidget() {
|
||||
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
val prefs = currentState<MutablePreferences>()
|
||||
|
||||
val imageUUID = prefs[kImageUUID]
|
||||
val subtitle = prefs[kSubtitleText]
|
||||
val deeplinkURL = prefs[kDeeplinkURL]?.toUri()
|
||||
val widgetState = prefs[kWidgetState]
|
||||
var bitmap: Bitmap? = null
|
||||
|
||||
if (imageUUID != null) {
|
||||
// fetch a random photo from server
|
||||
val file = File(context.cacheDir, imageFilename(imageUUID))
|
||||
|
||||
if (file.exists()) {
|
||||
bitmap = loadScaledBitmap(file, 500, 500)
|
||||
}
|
||||
}
|
||||
|
||||
// WIDGET CONTENT
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.background(GlanceTheme.colors.background)
|
||||
.clickable {
|
||||
val intent = Intent(Intent.ACTION_VIEW, deeplinkURL ?: "immich://".toUri())
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
) {
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
provider = ImageProvider(bitmap),
|
||||
contentDescription = "Widget Image",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = GlanceModifier.fillMaxSize()
|
||||
)
|
||||
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Column(
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(Color.White),
|
||||
fontSize = 16.sp
|
||||
),
|
||||
modifier = GlanceModifier
|
||||
.background(ColorProvider(Color(0x99000000))) // 60% black
|
||||
.padding(8.dp)
|
||||
.cornerRadius(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = GlanceModifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.splash),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
if (widgetState == WidgetState.LOG_IN.toString()) {
|
||||
Box(
|
||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Log in to your Immich server", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = GlanceModifier.fillMaxWidth().padding(16.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = GlanceModifier.size(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||
|
||||
Text("Loading widget...", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import app.alextran.immich.widget.model.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RandomReceiver : GlanceAppWidgetReceiver() {
|
||||
override val glanceAppWidget = PhotoWidget()
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
|
||||
appWidgetIds.forEach { widgetID ->
|
||||
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
||||
|
||||
// Launch coroutine to setup a single shot if the app requested the update
|
||||
if (fromMainApp) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val provider = ComponentName(context, RandomReceiver::class.java)
|
||||
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
||||
|
||||
glanceIds.forEach { widgetID ->
|
||||
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.RANDOM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
appWidgetIds.forEach { id ->
|
||||
ImageDownloadWorker.cancel(context, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package app.alextran.immich.widget.configure
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
|
||||
|
||||
data class DropdownItem (
|
||||
val label: String,
|
||||
val id: String,
|
||||
)
|
||||
|
||||
// Creating a composable to display a drop down menu
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Dropdown(items: List<DropdownItem>,
|
||||
selectedItem: DropdownItem?,
|
||||
onItemSelected: (DropdownItem) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(selectedItem?.label ?: items[0].label) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded && enabled },
|
||||
) {
|
||||
|
||||
TextField(
|
||||
value = selectedOption,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
enabled = enabled,
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
items.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option.label, color = MaterialTheme.colorScheme.onSurface) },
|
||||
onClick = {
|
||||
selectedOption = option.label
|
||||
onItemSelected(option)
|
||||
|
||||
expanded = false
|
||||
},
|
||||
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package app.alextran.immich.widget.configure
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun LightDarkTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
val colorScheme = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme ->
|
||||
dynamicDarkColorScheme(context)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme ->
|
||||
dynamicLightColorScheme(context)
|
||||
isDarkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package app.alextran.immich.widget.configure
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import app.alextran.immich.widget.ImageDownloadWorker
|
||||
import app.alextran.immich.widget.ImmichAPI
|
||||
import app.alextran.immich.widget.model.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class RandomConfigure : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Get widget ID from intent
|
||||
val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val glanceId = GlanceAppWidgetManager(applicationContext)
|
||||
.getGlanceIdBy(appWidgetId)
|
||||
|
||||
setContent {
|
||||
LightDarkTheme {
|
||||
RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = {
|
||||
finish()
|
||||
Log.w("WIDGET_ACTIVITY", "SAVING")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) {
|
||||
|
||||
var selectedAlbum by remember { mutableStateOf<DropdownItem?>(null) }
|
||||
var showAlbumName by remember { mutableStateOf(false) }
|
||||
var availableAlbums by remember { mutableStateOf<List<DropdownItem>>(listOf()) }
|
||||
var state by remember { mutableStateOf(WidgetConfigState.LOADING) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// get albums from server
|
||||
val serverCfg = ImmichAPI.getServerConfig(context)
|
||||
|
||||
if (serverCfg == null) {
|
||||
state = WidgetConfigState.LOG_IN
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val api = ImmichAPI(serverCfg)
|
||||
|
||||
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||
val currentAlbumId = currentState[kSelectedAlbum] ?: "NONE"
|
||||
val currentAlbumName = currentState[kSelectedAlbumName] ?: "None"
|
||||
var albumItems: List<DropdownItem>
|
||||
|
||||
try {
|
||||
albumItems = api.fetchAlbums().map {
|
||||
DropdownItem(it.albumName, it.id)
|
||||
}
|
||||
|
||||
state = WidgetConfigState.SUCCESS
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.e("WidgetWorker", "Error fetching albums: ${e.message}")
|
||||
|
||||
state = WidgetConfigState.NO_CONNECTION
|
||||
albumItems = listOf(DropdownItem(currentAlbumName, currentAlbumId))
|
||||
}
|
||||
|
||||
availableAlbums = listOf(DropdownItem("None", "NONE"), DropdownItem("Favorites", "FAVORITES")) + albumItems
|
||||
|
||||
// load selected configuration
|
||||
val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId }
|
||||
selectedAlbum = albumEntity ?: availableAlbums.first()
|
||||
|
||||
// load showAlbumName
|
||||
showAlbumName = currentState[kShowAlbumName] == true
|
||||
}
|
||||
|
||||
suspend fun saveConfiguration() {
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs[kSelectedAlbum] = selectedAlbum?.id ?: ""
|
||||
prefs[kSelectedAlbumName] = selectedAlbum?.label ?: ""
|
||||
prefs[kShowAlbumName] = showAlbumName
|
||||
}
|
||||
|
||||
ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar (
|
||||
title = { Text("Widget Configuration") },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
saveConfiguration()
|
||||
onDone()
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Check, contentDescription = "Close", tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding), // Respect the top bar
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
when (state) {
|
||||
WidgetConfigState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||
WidgetConfigState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.")
|
||||
else -> {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
// no connection warning
|
||||
if (state == WidgetConfigState.NO_CONNECTION) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.errorContainer)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = "Warning",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "No connection to the server is available. Please try again later.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text("Album")
|
||||
Dropdown(
|
||||
items = availableAlbums,
|
||||
selectedItem = selectedAlbum,
|
||||
onItemSelected = { selectedAlbum = it },
|
||||
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "Show Album Name")
|
||||
Switch(
|
||||
checked = showAlbumName,
|
||||
onCheckedChange = { showAlbumName = it },
|
||||
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package app.alextran.immich.widget.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.datastore.preferences.core.*
|
||||
|
||||
// MARK: Immich Entities
|
||||
|
||||
enum class AssetType {
|
||||
IMAGE, VIDEO, AUDIO, OTHER
|
||||
}
|
||||
|
||||
data class Asset(
|
||||
val id: String,
|
||||
val type: AssetType,
|
||||
)
|
||||
|
||||
data class SearchFilters(
|
||||
var type: AssetType = AssetType.IMAGE,
|
||||
val size: Int = 1,
|
||||
var albumIds: List<String> = listOf(),
|
||||
var isFavorite: Boolean? = null
|
||||
)
|
||||
|
||||
data class MemoryResult(
|
||||
val id: String,
|
||||
var assets: List<Asset>,
|
||||
val type: String,
|
||||
val data: MemoryData
|
||||
) {
|
||||
data class MemoryData(val year: Int)
|
||||
}
|
||||
|
||||
data class Album(
|
||||
val id: String,
|
||||
val albumName: String
|
||||
)
|
||||
|
||||
// MARK: Widget Specific
|
||||
|
||||
enum class WidgetType {
|
||||
RANDOM, MEMORIES;
|
||||
}
|
||||
|
||||
enum class WidgetState {
|
||||
LOADING, SUCCESS, LOG_IN;
|
||||
}
|
||||
|
||||
enum class WidgetConfigState {
|
||||
LOADING, SUCCESS, LOG_IN, NO_CONNECTION
|
||||
}
|
||||
|
||||
data class WidgetEntry (
|
||||
val image: Bitmap,
|
||||
val subtitle: String?,
|
||||
val deeplink: String?
|
||||
)
|
||||
|
||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
||||
|
||||
// MARK: Widget State Keys
|
||||
val kImageUUID = stringPreferencesKey("uuid")
|
||||
val kSubtitleText = stringPreferencesKey("subtitle")
|
||||
val kNow = longPreferencesKey("now")
|
||||
val kWidgetState = stringPreferencesKey("state")
|
||||
val kSelectedAlbum = stringPreferencesKey("albumID")
|
||||
val kSelectedAlbumName = stringPreferencesKey("albumName")
|
||||
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
||||
val kDeeplinkURL = stringPreferencesKey("deeplink")
|
||||
|
||||
const val kWorkerWidgetType = "widgetType"
|
||||
const val kWorkerWidgetID = "widgetId"
|
||||
const val kTriggeredFromApp = "triggeredFromApp"
|
||||
|
||||
fun imageFilename(id: String): String {
|
||||
return "widget_image_$id.jpg"
|
||||
}
|
||||
|
||||
fun assetDeeplink(asset: Asset): String {
|
||||
return "immich://asset?id=${asset.id}"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="memory_widget_title">Memories</string>
|
||||
<string name="random_widget_title">Random</string>
|
||||
|
||||
<string name="memory_widget_description">See memories from Immich.</string>
|
||||
<string name="random_widget_description">View a random image from your library or a specific album.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="110dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="1200000"
|
||||
android:description="@string/memory_widget_description"
|
||||
android:previewImage="@drawable/memory_preview"
|
||||
/>
|
||||
@@ -0,0 +1,13 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="110dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="1200000"
|
||||
android:configure="app.alextran.immich.widget.configure.RandomConfigure"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||
tools:targetApi="28"
|
||||
android:description="@string/random_widget_description"
|
||||
android:previewImage="@drawable/random_preview"
|
||||
/>
|
||||
@@ -1 +1 @@
|
||||
version: '>=1.29.0 <1.30.0'
|
||||
version: '>=1.29.0 <=1.30.0'
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase {
|
||||
return rules;
|
||||
}
|
||||
|
||||
static makeCode(String name, LintOptions options) => LintCode(
|
||||
static LintCode makeCode(String name, LintOptions options) => LintCode(
|
||||
name: name,
|
||||
problemMessage: options.json["message"] as String,
|
||||
errorSeverity: ErrorSeverity.WARNING,
|
||||
|
||||
@@ -14,19 +14,6 @@ struct ImageEntry: TimelineEntry {
|
||||
var deepLink: URL? = nil
|
||||
}
|
||||
|
||||
// Resizes the stored image to a maximum width of 450 pixels
|
||||
mutating func resize() {
|
||||
if image == nil || image!.size.height < 450 || image!.size.width < 450 {
|
||||
return
|
||||
}
|
||||
|
||||
image = image?.resized(toWidth: 450)
|
||||
|
||||
if image == nil {
|
||||
metadata.error = .unableToResize
|
||||
}
|
||||
}
|
||||
|
||||
static func build(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
@@ -83,7 +70,10 @@ struct ImageEntry: TimelineEntry {
|
||||
|
||||
guard let imageData = try? Data(contentsOf: imageURL),
|
||||
let metadataJSON = try? Data(contentsOf: metadataURL),
|
||||
let decodedMetadata = try? JSONDecoder().decode(Metadata.self, from: metadataJSON)
|
||||
let decodedMetadata = try? JSONDecoder().decode(
|
||||
Metadata.self,
|
||||
from: metadataJSON
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
@@ -98,7 +88,7 @@ struct ImageEntry: TimelineEntry {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func handleCacheFallback(
|
||||
static func handleError(
|
||||
for key: String,
|
||||
error: WidgetError = .fetchFailed
|
||||
) -> Timeline<ImageEntry> {
|
||||
@@ -108,35 +98,32 @@ struct ImageEntry: TimelineEntry {
|
||||
metadata: EntryMetadata(error: error)
|
||||
)
|
||||
|
||||
// skip cache if album not found or no login
|
||||
// we want to show these errors to the user since without intervention,
|
||||
// use cache if generic failed error
|
||||
// we want to show the other errors to the user since without intervention,
|
||||
// it will never succeed
|
||||
if error != .noLogin && error != .albumNotFound {
|
||||
if let cachedEntry = ImageEntry.loadCached(for: key) {
|
||||
timelineEntry = cachedEntry
|
||||
}
|
||||
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
|
||||
{
|
||||
timelineEntry = cachedEntry
|
||||
}
|
||||
|
||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
albumId: String? = nil,
|
||||
filter: SearchFilter = Album.NONE.filter,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
||||
var entries: [ImageEntry] = []
|
||||
let albumIds = albumId != nil ? [albumId!] : []
|
||||
|
||||
let randomAssets = try await api.fetchSearchResults(
|
||||
with: SearchFilters(size: count, albumIds: albumIds)
|
||||
)
|
||||
let randomAssets = try await api.fetchSearchResults(with: filter)
|
||||
|
||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
extension Image {
|
||||
@ViewBuilder
|
||||
func tintedWidgetImageModifier() -> some View {
|
||||
if #available(iOS 18.0, *) {
|
||||
self
|
||||
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImmichWidgetView: View {
|
||||
var entry: ImageEntry
|
||||
|
||||
@@ -8,6 +20,7 @@ struct ImmichWidgetView: View {
|
||||
if entry.image == nil {
|
||||
VStack {
|
||||
Image("LaunchImage")
|
||||
.tintedWidgetImageModifier()
|
||||
Text(entry.metadata.error?.errorDescription ?? "")
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -19,7 +32,9 @@ struct ImmichWidgetView: View {
|
||||
Color.clear.overlay(
|
||||
Image(uiImage: entry.image!)
|
||||
.resizable()
|
||||
.tintedWidgetImageModifier()
|
||||
.scaledToFill()
|
||||
|
||||
)
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
@@ -2,14 +2,20 @@ import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||
|
||||
enum WidgetError: Error, Codable {
|
||||
case noLogin
|
||||
case fetchFailed
|
||||
case unknown
|
||||
case albumNotFound
|
||||
case noAssetsAvailable
|
||||
}
|
||||
|
||||
enum FetchError: Error {
|
||||
case unableToResize
|
||||
case invalidImage
|
||||
case invalidURL
|
||||
case fetchFailed
|
||||
}
|
||||
|
||||
extension WidgetError: LocalizedError {
|
||||
@@ -24,14 +30,8 @@ extension WidgetError: LocalizedError {
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
case .invalidURL:
|
||||
return "An invalid URL was used"
|
||||
|
||||
case .invalidImage:
|
||||
return "An invalid image was received"
|
||||
|
||||
default:
|
||||
return "An unknown error occured"
|
||||
case .noAssetsAvailable:
|
||||
return "No assets available"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,11 @@ struct Asset: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchFilters: Codable {
|
||||
var type: AssetType = .image
|
||||
let size: Int
|
||||
struct SearchFilter: Codable {
|
||||
var type = AssetType.image
|
||||
var size = 1
|
||||
var albumIds: [String] = []
|
||||
var isFavorite: Bool? = nil
|
||||
}
|
||||
|
||||
struct MemoryResult: Codable {
|
||||
@@ -70,12 +71,35 @@ struct MemoryResult: Codable {
|
||||
let data: MemoryData
|
||||
}
|
||||
|
||||
struct Album: Codable {
|
||||
struct Album: Codable, Equatable {
|
||||
let id: String
|
||||
let albumName: String
|
||||
}
|
||||
|
||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||
static let NONE = Album(id: "NONE", albumName: "None")
|
||||
static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
|
||||
|
||||
var filter: SearchFilter {
|
||||
switch self {
|
||||
case Album.NONE:
|
||||
return SearchFilter()
|
||||
case Album.FAVORITES:
|
||||
return SearchFilter(isFavorite: true)
|
||||
|
||||
// regular album
|
||||
default:
|
||||
return SearchFilter(albumIds: [id])
|
||||
}
|
||||
}
|
||||
|
||||
var isVirtual: Bool {
|
||||
switch self {
|
||||
case Album.NONE, Album.FAVORITES:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
@@ -132,7 +156,8 @@ class ImmichAPI {
|
||||
return components?.url
|
||||
}
|
||||
|
||||
func fetchSearchResults(with filters: SearchFilters) async throws
|
||||
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||
async throws
|
||||
-> [Asset]
|
||||
{
|
||||
// get URL
|
||||
@@ -178,7 +203,7 @@ class ImmichAPI {
|
||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
||||
}
|
||||
|
||||
func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
|
||||
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage {
|
||||
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||
|
||||
@@ -199,7 +224,7 @@ class ImmichAPI {
|
||||
|
||||
let decodeOptions: [NSString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 512,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
]
|
||||
|
||||
|
||||
@@ -23,26 +23,27 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
|
||||
Task {
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!)
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||
else {
|
||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
}
|
||||
|
||||
for memory in memories {
|
||||
if let asset = memory.assets.first(where: { $0.type == .image }),
|
||||
var entry = try? await ImageEntry.build(
|
||||
let entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: 0,
|
||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||
)
|
||||
{
|
||||
entry.resize()
|
||||
completion(entry)
|
||||
return
|
||||
}
|
||||
@@ -50,20 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
|
||||
// fallback to random image
|
||||
guard
|
||||
let randomImage = try? await api.fetchSearchResults(
|
||||
with: SearchFilters(size: 1)
|
||||
).first,
|
||||
var imageEntry = try? await ImageEntry.build(
|
||||
let randomImage = try? await api.fetchSearchResults().first,
|
||||
let imageEntry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
}
|
||||
|
||||
imageEntry.resize()
|
||||
completion(imageEntry)
|
||||
}
|
||||
}
|
||||
@@ -80,7 +78,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(
|
||||
ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
||||
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -119,25 +117,28 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
// If we didnt add any memory images (some failure occured or no images in memory),
|
||||
// default to 12 hours of random photos
|
||||
if entries.count == 0 {
|
||||
entries.append(
|
||||
contentsOf: (try? await generateRandomEntries(
|
||||
// this must be a do/catch since we need to
|
||||
// distinguish between a network fail and an empty search
|
||||
do {
|
||||
let search = try await generateRandomEntries(
|
||||
api: api,
|
||||
now: now,
|
||||
count: 12
|
||||
)) ?? []
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// If we fail to fetch images, we still want to add an entry
|
||||
// with a nil image and an error
|
||||
if entries.count == 0 {
|
||||
completion(ImageEntry.handleCacheFallback(for: cacheKey))
|
||||
return
|
||||
}
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if search.count == 0 {
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Resize all images to something that can be stored by iOS
|
||||
for i in entries.indices {
|
||||
entries[i].resize()
|
||||
entries.append(contentsOf: search)
|
||||
} catch {
|
||||
completion(ImageEntry.handleError(for: cacheKey))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// cache the last image
|
||||
|
||||
@@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
||||
|
||||
struct AlbumQuery: EntityQuery {
|
||||
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
||||
// use cached albums to search
|
||||
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||
albums.insert(NO_ALBUM, at: 0)
|
||||
|
||||
return albums.filter {
|
||||
return await suggestedEntities().filter {
|
||||
identifiers.contains($0.id)
|
||||
}
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [Album] {
|
||||
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
||||
albums.insert(NO_ALBUM, at: 0)
|
||||
func suggestedEntities() async -> [Album] {
|
||||
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||
|
||||
return albums
|
||||
let options =
|
||||
[
|
||||
NONE,
|
||||
FAVORITES,
|
||||
] + albums
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +36,6 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
let NO_ALBUM = Album(id: "NONE", albumName: "None")
|
||||
|
||||
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Select Album" }
|
||||
static var description: IntentDescription {
|
||||
@@ -64,26 +63,25 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
-> ImageEntry
|
||||
{
|
||||
let cacheKey = "random_none_\(context.family.rawValue)"
|
||||
|
||||
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
||||
.first!
|
||||
}
|
||||
|
||||
guard
|
||||
let randomImage = try? await api.fetchSearchResults(
|
||||
with: SearchFilters(size: 1)
|
||||
with: Album.NONE.filter
|
||||
).first,
|
||||
var entry = try? await ImageEntry.build(
|
||||
let entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
return ImageEntry.handleCacheFallback(for: cacheKey).entries.first!
|
||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||
}
|
||||
|
||||
entry.resize()
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
@@ -97,52 +95,36 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
let now = Date()
|
||||
|
||||
// nil if album is NONE or nil
|
||||
let albumId =
|
||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
||||
let albumName: String? =
|
||||
albumId != nil ? configuration.album?.albumName : nil
|
||||
let album = configuration.album ?? Album.NONE
|
||||
let albumName = album.isVirtual ? nil : album.albumName
|
||||
|
||||
let cacheKey = "random_\(albumId ?? "none")_\(context.family.rawValue)"
|
||||
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
|
||||
|
||||
// If we don't have a server config, return an entry with an error
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
||||
}
|
||||
|
||||
if albumId != nil {
|
||||
// make sure the album exists on server, otherwise show error
|
||||
guard let albums = try? await api.fetchAlbums() else {
|
||||
return ImageEntry.handleCacheFallback(for: cacheKey)
|
||||
}
|
||||
|
||||
if !albums.contains(where: { $0.id == albumId }) {
|
||||
return ImageEntry.handleCacheFallback(
|
||||
for: cacheKey,
|
||||
error: .albumNotFound
|
||||
)
|
||||
}
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||
}
|
||||
|
||||
// build entries
|
||||
entries.append(
|
||||
contentsOf: (try? await generateRandomEntries(
|
||||
// this must be a do/catch since we need to
|
||||
// distinguish between a network fail and an empty search
|
||||
do {
|
||||
let search = try await generateRandomEntries(
|
||||
api: api,
|
||||
now: now,
|
||||
count: 12,
|
||||
albumId: albumId,
|
||||
filter: album.filter,
|
||||
subtitle: configuration.showAlbumName ? albumName : nil
|
||||
))
|
||||
?? []
|
||||
)
|
||||
)
|
||||
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if entries.count == 0 {
|
||||
return ImageEntry.handleCacheFallback(for: cacheKey)
|
||||
}
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if search.count == 0 {
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
}
|
||||
|
||||
// Resize all images to something that can be stored by iOS
|
||||
for i in entries.indices {
|
||||
entries[i].resize()
|
||||
entries.append(contentsOf: search)
|
||||
} catch {
|
||||
return ImageEntry.handleError(for: cacheKey)
|
||||
}
|
||||
|
||||
// cache the last image
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}
|
||||
@@ -16,6 +16,11 @@ const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
||||
// Secure storage keys
|
||||
const String kSecuredPinCode = "secured_pin_code";
|
||||
|
||||
// background_downloader task groups
|
||||
const String kManualUploadGroup = 'manual_upload_group';
|
||||
const String kBackupGroup = 'backup_group';
|
||||
const String kBackupLivePhotoGroup = 'backup_live_photo_group';
|
||||
|
||||
// Timeline constants
|
||||
const int kTimelineNoneSegmentSize = 120;
|
||||
const int kTimelineAssetLoadBatchSize = 256;
|
||||
@@ -28,7 +33,11 @@ const String appShareGroupId = "group.app.immich.share";
|
||||
|
||||
// add widget identifiers here for new widgets
|
||||
// these are used to force a widget refresh
|
||||
const List<String> kWidgetNames = [
|
||||
'com.immich.widget.random',
|
||||
'com.immich.widget.memory',
|
||||
// (iOSName, androidFQDN)
|
||||
const List<(String, String)> kWidgetNames = [
|
||||
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
|
||||
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
|
||||
];
|
||||
|
||||
const double kUploadStatusFailed = -1.0;
|
||||
const double kUploadStatusCanceled = -2.0;
|
||||
|
||||
@@ -86,4 +86,32 @@ class RemoteAlbum {
|
||||
assetCount.hashCode ^
|
||||
ownerName.hashCode;
|
||||
}
|
||||
|
||||
RemoteAlbum copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
int? assetCount,
|
||||
String? ownerName,
|
||||
}) {
|
||||
return RemoteAlbum(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
||||
order: order ?? this.order,
|
||||
assetCount: assetCount ?? this.assetCount,
|
||||
ownerName: ownerName ?? this.ownerName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class LocalAsset extends BaseAsset {
|
||||
}''';
|
||||
}
|
||||
|
||||
// Not checking for remoteId here
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! LocalAsset) return false;
|
||||
|
||||
@@ -14,6 +14,8 @@ class RemoteAsset extends BaseAsset {
|
||||
final String? thumbHash;
|
||||
final AssetVisibility visibility;
|
||||
final String ownerId;
|
||||
final String? stackId;
|
||||
final int stackCount;
|
||||
|
||||
const RemoteAsset({
|
||||
required this.id,
|
||||
@@ -31,6 +33,8 @@ class RemoteAsset extends BaseAsset {
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
this.stackCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -56,9 +60,14 @@ class RemoteAsset extends BaseAsset {
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
visibility: $visibility,
|
||||
stackId: ${stackId ?? "<NA>"},
|
||||
stackCount: $stackCount,
|
||||
checksum: $checksum,
|
||||
livePhotoVideoId: ${livePhotoVideoId ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
// Not checking for localId here
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! RemoteAsset) return false;
|
||||
@@ -67,7 +76,9 @@ class RemoteAsset extends BaseAsset {
|
||||
id == other.id &&
|
||||
ownerId == other.ownerId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility;
|
||||
visibility == other.visibility &&
|
||||
stackId == other.stackId &&
|
||||
stackCount == other.stackCount;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -77,7 +88,9 @@ class RemoteAsset extends BaseAsset {
|
||||
ownerId.hashCode ^
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode;
|
||||
visibility.hashCode ^
|
||||
stackId.hashCode ^
|
||||
stackCount.hashCode;
|
||||
|
||||
RemoteAsset copyWith({
|
||||
String? id,
|
||||
@@ -95,6 +108,8 @@ class RemoteAsset extends BaseAsset {
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
int? stackCount,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -112,6 +127,8 @@ class RemoteAsset extends BaseAsset {
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
stackCount: stackCount ?? this.stackCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,21 @@ class DriftMemory {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
|
||||
return '''Memory {
|
||||
id: $id,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
deletedAt: ${deletedAt ?? "<NA>"},
|
||||
ownerId: $ownerId,
|
||||
type: $type,
|
||||
data: $data,
|
||||
isSaved: $isSaved,
|
||||
memoryAt: $memoryAt,
|
||||
seenAt: ${seenAt ?? "<NA>"},
|
||||
showAt: ${showAt ?? "<NA>"},
|
||||
hideAt: ${hideAt ?? "<NA>"},
|
||||
assets: $assets
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Person {
|
||||
const Person({
|
||||
// TODO: Remove PersonDto once Isar is removed
|
||||
class PersonDto {
|
||||
const PersonDto({
|
||||
required this.id,
|
||||
this.birthDate,
|
||||
required this.isHidden,
|
||||
@@ -22,7 +23,7 @@ class Person {
|
||||
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
Person copyWith({
|
||||
PersonDto copyWith({
|
||||
String? id,
|
||||
DateTime? birthDate,
|
||||
bool? isHidden,
|
||||
@@ -30,7 +31,7 @@ class Person {
|
||||
String? thumbnailPath,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return Person(
|
||||
return PersonDto(
|
||||
id: id ?? this.id,
|
||||
birthDate: birthDate ?? this.birthDate,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
@@ -51,8 +52,8 @@ class Person {
|
||||
};
|
||||
}
|
||||
|
||||
factory Person.fromMap(Map<String, dynamic> map) {
|
||||
return Person(
|
||||
factory PersonDto.fromMap(Map<String, dynamic> map) {
|
||||
return PersonDto(
|
||||
id: map['id'] as String,
|
||||
birthDate: map['birthDate'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
||||
@@ -68,11 +69,11 @@ class Person {
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Person.fromJson(String source) =>
|
||||
Person.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
factory PersonDto.fromJson(String source) =>
|
||||
PersonDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Person other) {
|
||||
bool operator ==(covariant PersonDto other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
@@ -93,3 +94,109 @@ class Person {
|
||||
updatedAt.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Model for a person stored in the server
|
||||
class Person {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String ownerId;
|
||||
final String name;
|
||||
final String? faceAssetId;
|
||||
final String thumbnailPath;
|
||||
final bool isFavorite;
|
||||
final bool isHidden;
|
||||
final String? color;
|
||||
final DateTime? birthDate;
|
||||
|
||||
const Person({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.ownerId,
|
||||
required this.name,
|
||||
this.faceAssetId,
|
||||
required this.thumbnailPath,
|
||||
required this.isFavorite,
|
||||
required this.isHidden,
|
||||
required this.color,
|
||||
this.birthDate,
|
||||
});
|
||||
|
||||
Person copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? ownerId,
|
||||
String? name,
|
||||
String? faceAssetId,
|
||||
String? thumbnailPath,
|
||||
bool? isFavorite,
|
||||
bool? isHidden,
|
||||
String? color,
|
||||
DateTime? birthDate,
|
||||
}) {
|
||||
return Person(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
name: name ?? this.name,
|
||||
faceAssetId: faceAssetId ?? this.faceAssetId,
|
||||
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
color: color ?? this.color,
|
||||
birthDate: birthDate ?? this.birthDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Person {
|
||||
id: $id,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
ownerId: $ownerId,
|
||||
name: $name,
|
||||
faceAssetId: ${faceAssetId ?? "<NA>"},
|
||||
thumbnailPath: $thumbnailPath,
|
||||
isFavorite: $isFavorite,
|
||||
isHidden: $isHidden,
|
||||
color: ${color ?? "<NA>"},
|
||||
birthDate: ${birthDate ?? "<NA>"}
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Person other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.createdAt == createdAt &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.ownerId == ownerId &&
|
||||
other.name == name &&
|
||||
other.faceAssetId == faceAssetId &&
|
||||
other.thumbnailPath == thumbnailPath &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isHidden == isHidden &&
|
||||
other.color == color &&
|
||||
other.birthDate == birthDate;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
name.hashCode ^
|
||||
faceAssetId.hashCode ^
|
||||
thumbnailPath.hashCode ^
|
||||
isFavorite.hashCode ^
|
||||
isHidden.hashCode ^
|
||||
color.hashCode ^
|
||||
birthDate.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
class SearchResult {
|
||||
final List<BaseAsset> assets;
|
||||
final int? nextPage;
|
||||
|
||||
const SearchResult({
|
||||
required this.assets,
|
||||
this.nextPage,
|
||||
});
|
||||
|
||||
int get totalAssets => assets.length;
|
||||
|
||||
SearchResult copyWith({
|
||||
List<BaseAsset>? assets,
|
||||
int? nextPage,
|
||||
}) {
|
||||
return SearchResult(
|
||||
assets: assets ?? this.assets,
|
||||
nextPage: nextPage ?? this.nextPage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant SearchResult other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return listEquals(other.assets, assets) && other.nextPage == nextPage;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assets.hashCode ^ nextPage.hashCode;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
// Model for a stack stored in the server
|
||||
class Stack {
|
||||
final String id;
|
||||
@@ -32,34 +30,15 @@ class Stack {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||
'ownerId': ownerId,
|
||||
'primaryAssetId': primaryAssetId,
|
||||
};
|
||||
}
|
||||
|
||||
factory Stack.fromMap(Map<String, dynamic> map) {
|
||||
return Stack(
|
||||
id: map['id'] as String,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
||||
ownerId: map['ownerId'] as String,
|
||||
primaryAssetId: map['primaryAssetId'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Stack.fromJson(String source) =>
|
||||
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
|
||||
return '''Stack {
|
||||
id: $id,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
ownerId: $ownerId,
|
||||
primaryAssetId: $primaryAssetId
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -82,3 +61,27 @@ class Stack {
|
||||
primaryAssetId.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class StackResponse {
|
||||
final String id;
|
||||
final String primaryAssetId;
|
||||
final List<String> assetIds;
|
||||
|
||||
const StackResponse({
|
||||
required this.id,
|
||||
required this.primaryAssetId,
|
||||
required this.assetIds,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant StackResponse other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.primaryAssetId == primaryAssetId &&
|
||||
other.assetIds == assetIds;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ primaryAssetId.hashCode ^ assetIds.hashCode;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@ enum StoreKey<T> {
|
||||
manageLocalMediaAndroid<bool>._(137),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000);
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
betaPromptShown<bool>._(1001),
|
||||
betaTimeline<bool>._(1002),
|
||||
enableBackup<bool>._(1003);
|
||||
|
||||
const StoreKey._(this.id);
|
||||
final int id;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
|
||||
enum GroupAssetsBy {
|
||||
day,
|
||||
month,
|
||||
@@ -38,3 +40,7 @@ class TimeBucket extends Bucket {
|
||||
@override
|
||||
int get hashCode => super.hashCode ^ date.hashCode;
|
||||
}
|
||||
|
||||
class TimelineReloadEvent extends Event {
|
||||
const TimelineReloadEvent();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
|
||||
// TODO: Rename to User once Isar is removed
|
||||
@@ -123,3 +126,88 @@ quotaSizeInBytes: $quotaSizeInBytes,
|
||||
quotaUsageInBytes.hashCode ^
|
||||
quotaSizeInBytes.hashCode;
|
||||
}
|
||||
|
||||
class PartnerUserDto {
|
||||
final String id;
|
||||
final String email;
|
||||
final String name;
|
||||
final bool inTimeline;
|
||||
|
||||
final String? profileImagePath;
|
||||
|
||||
const PartnerUserDto({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.inTimeline,
|
||||
this.profileImagePath,
|
||||
});
|
||||
|
||||
PartnerUserDto copyWith({
|
||||
String? id,
|
||||
String? email,
|
||||
String? name,
|
||||
bool? inTimeline,
|
||||
String? profileImagePath,
|
||||
}) {
|
||||
return PartnerUserDto(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
inTimeline: inTimeline ?? this.inTimeline,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'email': email,
|
||||
'name': name,
|
||||
'inTimeline': inTimeline,
|
||||
'profileImagePath': profileImagePath,
|
||||
};
|
||||
}
|
||||
|
||||
factory PartnerUserDto.fromMap(Map<String, dynamic> map) {
|
||||
return PartnerUserDto(
|
||||
id: map['id'] as String,
|
||||
email: map['email'] as String,
|
||||
name: map['name'] as String,
|
||||
inTimeline: map['inTimeline'] as bool,
|
||||
profileImagePath: map['profileImagePath'] != null
|
||||
? map['profileImagePath'] as String
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PartnerUserDto.fromJson(String source) =>
|
||||
PartnerUserDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant PartnerUserDto other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.email == email &&
|
||||
other.name == name &&
|
||||
other.inTimeline == inTimeline &&
|
||||
other.profileImagePath == profileImagePath;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
email.hashCode ^
|
||||
name.hashCode ^
|
||||
inTimeline.hashCode ^
|
||||
profileImagePath.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,17 @@ class AssetService {
|
||||
: _remoteAssetRepository.watchAsset(id);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
|
||||
if (asset.stackId == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _remoteAssetRepository.getStackChildren(asset).then((assets) {
|
||||
// Include the primary asset in the stack as the first item
|
||||
return [asset, ...assets];
|
||||
});
|
||||
}
|
||||
|
||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||
if (!asset.hasRemote) {
|
||||
return null;
|
||||
|
||||
@@ -7,11 +7,15 @@ class LocalAlbumService {
|
||||
|
||||
const LocalAlbumService(this._repository);
|
||||
|
||||
Future<List<LocalAlbum>> getAll() {
|
||||
return _repository.getAll();
|
||||
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
|
||||
return _repository.getAll(sortBy: sortBy);
|
||||
}
|
||||
|
||||
Future<LocalAsset?> getThumbnail(String albumId) {
|
||||
return _repository.getThumbnail(albumId);
|
||||
}
|
||||
|
||||
Future<void> update(LocalAlbum album) {
|
||||
return _repository.upsert(album);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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';
|
||||
|
||||
class DriftPartnerService {
|
||||
final DriftPartnerRepository _driftPartnerRepository;
|
||||
final PartnerApiRepository _partnerApiRepository;
|
||||
|
||||
const DriftPartnerService(
|
||||
this._driftPartnerRepository,
|
||||
this._partnerApiRepository,
|
||||
);
|
||||
|
||||
Future<List<PartnerUserDto>> getSharedWith(String userId) {
|
||||
return _driftPartnerRepository.getSharedWith(userId);
|
||||
}
|
||||
|
||||
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||
return _driftPartnerRepository.getSharedBy(userId);
|
||||
}
|
||||
|
||||
Future<List<PartnerUserDto>> getAvailablePartners(
|
||||
String currentUserId,
|
||||
) async {
|
||||
final otherUsers =
|
||||
await _driftPartnerRepository.getAvailablePartners(currentUserId);
|
||||
final currentPartners =
|
||||
await _driftPartnerRepository.getSharedBy(currentUserId);
|
||||
final available = otherUsers.where((user) {
|
||||
return !currentPartners.any((partner) => partner.id == user.id);
|
||||
}).toList();
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
await _partnerApiRepository.update(
|
||||
partnerId,
|
||||
inTimeline: !partner.inTimeline,
|
||||
);
|
||||
|
||||
await _driftPartnerRepository.toggleShowInTimeline(partner, userId);
|
||||
}
|
||||
|
||||
Future<void> addPartner(String partnerId, String userId) async {
|
||||
await _partnerApiRepository.create(partnerId);
|
||||
await _driftPartnerRepository.create(partnerId, userId);
|
||||
}
|
||||
|
||||
Future<void> removePartner(String partnerId, String userId) async {
|
||||
await _partnerApiRepository.delete(partnerId);
|
||||
await _driftPartnerRepository.delete(partnerId, userId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
@@ -10,6 +14,10 @@ class RemoteAlbumService {
|
||||
|
||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||
|
||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||
return _repository.watchAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> getAll() {
|
||||
return _repository.getAll();
|
||||
}
|
||||
@@ -75,4 +83,68 @@ class RemoteAlbumService {
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? description,
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
}) async {
|
||||
final updatedAlbum = await _albumApiRepository.updateAlbum(
|
||||
albumId,
|
||||
name: name,
|
||||
description: description,
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order,
|
||||
);
|
||||
|
||||
// Update the local database
|
||||
await _repository.update(updatedAlbum);
|
||||
|
||||
return updatedAlbum;
|
||||
}
|
||||
|
||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||
return _repository.getDateRange(albumId);
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getSharedUsers(String albumId) {
|
||||
return _repository.getSharedUsers(albumId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
return _repository.getAssets(albumId);
|
||||
}
|
||||
|
||||
Future<int> addAssets({
|
||||
required String albumId,
|
||||
required List<String> assetIds,
|
||||
}) async {
|
||||
final album = await _albumApiRepository.addAssets(
|
||||
albumId,
|
||||
assetIds,
|
||||
);
|
||||
|
||||
await _repository.addAssets(albumId, album.added);
|
||||
|
||||
return album.added.length;
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) async {
|
||||
await _albumApiRepository.deleteAlbum(albumId);
|
||||
|
||||
await _repository.deleteAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<void> addUsers({
|
||||
required String albumId,
|
||||
required List<String> userIds,
|
||||
}) async {
|
||||
await _albumApiRepository.addUsers(albumId, userIds);
|
||||
|
||||
return _repository.addUsers(albumId, userIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||
|
||||
class SearchService {
|
||||
final _log = Logger("SearchService");
|
||||
final SearchApiRepository _searchApiRepository;
|
||||
|
||||
SearchService(this._searchApiRepository);
|
||||
|
||||
Future<List<String>?> getSearchSuggestions(
|
||||
SearchSuggestionType type, {
|
||||
String? country,
|
||||
String? state,
|
||||
String? make,
|
||||
String? model,
|
||||
}) async {
|
||||
try {
|
||||
return await _searchApiRepository.getSearchSuggestions(
|
||||
type,
|
||||
country: country,
|
||||
state: state,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to get search suggestions", e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<SearchResult?> search(SearchFilter filter, int page) async {
|
||||
try {
|
||||
final response = await _searchApiRepository.search(filter, page);
|
||||
|
||||
if (response == null || response.assets.items.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SearchResult(
|
||||
assets: response.assets.items.map((e) => e.toDto()).toList(),
|
||||
nextPage: response.assets.nextPage?.toInt(),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Failed to search for assets", error, stackTrace);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetResponseDto {
|
||||
RemoteAsset toDto() {
|
||||
return RemoteAsset(
|
||||
id: id,
|
||||
name: originalFileName,
|
||||
checksum: checksum,
|
||||
createdAt: fileCreatedAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
visibility: switch (visibility) {
|
||||
api.AssetVisibility.timeline => AssetVisibility.timeline,
|
||||
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
||||
api.AssetVisibility.archive => AssetVisibility.archive,
|
||||
api.AssetVisibility.locked => AssetVisibility.locked,
|
||||
_ => AssetVisibility.timeline,
|
||||
},
|
||||
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
|
||||
height: exifInfo?.exifImageHeight?.toInt(),
|
||||
width: exifInfo?.exifImageWidth?.toInt(),
|
||||
isFavorite: isFavorite,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
thumbHash: thumbhash,
|
||||
localId: null,
|
||||
type: type.toAssetType(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetTypeEnum {
|
||||
AssetType toAssetType() => switch (this) {
|
||||
AssetTypeEnum.IMAGE => AssetType.image,
|
||||
AssetTypeEnum.VIDEO => AssetType.video,
|
||||
AssetTypeEnum.AUDIO => AssetType.audio,
|
||||
AssetTypeEnum.OTHER => AssetType.other,
|
||||
_ => throw Exception('Unknown AssetType value: $this'),
|
||||
};
|
||||
}
|
||||
@@ -93,6 +93,8 @@ class StoreService {
|
||||
await _storeRepository.deleteAll();
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
|
||||
}
|
||||
|
||||
class StoreKeyNotFoundException implements Exception {
|
||||
|
||||
@@ -240,6 +240,10 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deleteUserMetadatasV1(
|
||||
data.cast(),
|
||||
);
|
||||
case SyncEntityType.personV1:
|
||||
return _syncStreamRepository.updatePeopleV1(data.cast());
|
||||
case SyncEntityType.personDeleteV1:
|
||||
return _syncStreamRepository.deletePeopleV1(data.cast());
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ class TimelineFactory {
|
||||
|
||||
TimelineService place(String place) =>
|
||||
TimelineService(_timelineRepository.place(place, groupBy));
|
||||
|
||||
TimelineService fromAssets(List<BaseAsset> assets) =>
|
||||
TimelineService(_timelineRepository.fromAssets(assets));
|
||||
}
|
||||
|
||||
class TimelineService {
|
||||
|
||||
@@ -4,13 +4,24 @@ import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||
import 'package:immich_mobile/utils/isolate.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
typedef SyncCallback = void Function();
|
||||
typedef SyncErrorCallback = void Function(String error);
|
||||
|
||||
class BackgroundSyncManager {
|
||||
final SyncCallback? onRemoteSyncStart;
|
||||
final SyncCallback? onRemoteSyncComplete;
|
||||
final SyncErrorCallback? onRemoteSyncError;
|
||||
|
||||
Cancelable<void>? _syncTask;
|
||||
Cancelable<void>? _syncWebsocketTask;
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
|
||||
BackgroundSyncManager();
|
||||
BackgroundSyncManager({
|
||||
this.onRemoteSyncStart,
|
||||
this.onRemoteSyncComplete,
|
||||
this.onRemoteSyncError,
|
||||
});
|
||||
|
||||
Future<void> cancel() {
|
||||
final futures = <Future>[];
|
||||
@@ -72,10 +83,16 @@ class BackgroundSyncManager {
|
||||
return _syncTask!.future;
|
||||
}
|
||||
|
||||
onRemoteSyncStart?.call();
|
||||
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
);
|
||||
return _syncTask!.whenComplete(() {
|
||||
onRemoteSyncComplete?.call();
|
||||
_syncTask = null;
|
||||
}).catchError((error) {
|
||||
onRemoteSyncError?.call(error.toString());
|
||||
_syncTask = null;
|
||||
});
|
||||
}
|
||||
@@ -84,14 +101,18 @@ class BackgroundSyncManager {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
|
||||
_syncWebsocketTask = runInIsolateGentle(
|
||||
computation: (ref) => ref
|
||||
.read(syncStreamServiceProvider)
|
||||
.handleWsAssetUploadReadyV1Batch(batchData),
|
||||
);
|
||||
_syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData);
|
||||
return _syncWebsocketTask!.whenComplete(() {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(
|
||||
List<dynamic> batchData,
|
||||
) =>
|
||||
runInIsolateGentle(
|
||||
computation: (ref) => ref
|
||||
.read(syncStreamServiceProvider)
|
||||
.handleWsAssetUploadReadyV1Batch(batchData),
|
||||
);
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
sealed class Event {
|
||||
class Event {
|
||||
const Event();
|
||||
}
|
||||
|
||||
class TimelineReloadEvent extends Event {
|
||||
const TimelineReloadEvent();
|
||||
}
|
||||
|
||||
class ViewerOpenBottomSheetEvent extends Event {
|
||||
const ViewerOpenBottomSheetEvent();
|
||||
}
|
||||
|
||||
class EventStream {
|
||||
EventStream._();
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ class BackupAlbum {
|
||||
BackupAlbum(this.id, this.lastBackup, this.selection);
|
||||
|
||||
Id get isarId => fastHash(id);
|
||||
|
||||
BackupAlbum copyWith({
|
||||
String? id,
|
||||
DateTime? lastBackup,
|
||||
BackupSelection? selection,
|
||||
}) {
|
||||
return BackupAlbum(
|
||||
id ?? this.id,
|
||||
lastBackup ?? this.lastBackup,
|
||||
selection ?? this.selection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum BackupSelection {
|
||||
|
||||
@@ -33,6 +33,10 @@ extension ContextHelper on BuildContext {
|
||||
|
||||
// Returns the current Primary color of the Theme
|
||||
Color get primaryColor => themeData.colorScheme.primary;
|
||||
Color get logoYellow => const Color.fromARGB(255, 255, 184, 0);
|
||||
Color get logoRed => const Color.fromARGB(255, 230, 65, 30);
|
||||
Color get logoPink => const Color.fromARGB(255, 222, 127, 179);
|
||||
Color get logoGreen => const Color.fromARGB(255, 49, 164, 82);
|
||||
|
||||
// Returns the Scaffold background color of the Theme
|
||||
Color get scaffoldBackgroundColor => colorScheme.surface;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:ui';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension TimeAgoExtension on DateTime {
|
||||
/// Displays the time difference of this [DateTime] object to the current time as a [String]
|
||||
String timeAgo({bool numericDates = true}) {
|
||||
@@ -35,3 +38,56 @@ extension TimeAgoExtension on DateTime {
|
||||
return '${(difference.inDays / 365).floor()} years ago';
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to format date ranges according to UI requirements
|
||||
extension DateRangeFormatting on DateTime {
|
||||
/// Formats a date range according to specific rules:
|
||||
/// - Single date of this year: "Aug 28"
|
||||
/// - Single date of other year: "Aug 28, 2023"
|
||||
/// - Date range of this year: "Mar 23-May 31"
|
||||
/// - Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||
/// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||
static String formatDateRange(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
Locale? locale,
|
||||
) {
|
||||
final now = DateTime.now();
|
||||
final currentYear = now.year;
|
||||
final localeString = locale?.toString() ?? 'en_US';
|
||||
|
||||
// Check if it's a single date (same day)
|
||||
if (startDate.year == endDate.year &&
|
||||
startDate.month == endDate.month &&
|
||||
startDate.day == endDate.day) {
|
||||
if (startDate.year == currentYear) {
|
||||
// Single date of this year: "Aug 28"
|
||||
return DateFormat.MMMd(localeString).format(startDate);
|
||||
} else {
|
||||
// Single date of other year: "Aug 28, 2023"
|
||||
return DateFormat.yMMMd(localeString).format(startDate);
|
||||
}
|
||||
}
|
||||
|
||||
// It's a date range
|
||||
if (startDate.year == endDate.year) {
|
||||
// Same year
|
||||
if (startDate.year == currentYear) {
|
||||
// Date range of this year: "Mar 23-May 31"
|
||||
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted';
|
||||
} else {
|
||||
// Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted, ${startDate.year}';
|
||||
}
|
||||
} else {
|
||||
// Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||
final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'remote_asset.entity.dart';
|
||||
import 'local_asset.entity.dart';
|
||||
import 'stack.entity.dart';
|
||||
|
||||
mergedAsset: SELECT * FROM
|
||||
(
|
||||
@@ -18,13 +19,33 @@ mergedAsset: SELECT * FROM
|
||||
rae.checksum,
|
||||
rae.owner_id,
|
||||
rae.live_photo_video_id,
|
||||
0 as orientation
|
||||
0 as orientation,
|
||||
rae.stack_id,
|
||||
COALESCE(stack_count.total_count, 0) AS stack_count
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||
LEFT JOIN
|
||||
stack_entity se ON rae.stack_id = se.id
|
||||
LEFT JOIN
|
||||
(SELECT
|
||||
stack_id,
|
||||
COUNT(*) AS total_count
|
||||
FROM remote_asset_entity
|
||||
WHERE deleted_at IS NULL
|
||||
AND visibility = 0
|
||||
AND stack_id IS NOT NULL
|
||||
GROUP BY stack_id
|
||||
) AS stack_count ON rae.stack_id = stack_count.stack_id
|
||||
WHERE
|
||||
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
|
||||
rae.deleted_at IS NULL
|
||||
AND rae.visibility = 0
|
||||
AND rae.owner_id in ?
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
OR rae.id = se.primary_asset_id
|
||||
)
|
||||
UNION ALL
|
||||
SELECT
|
||||
NULL as remote_id,
|
||||
@@ -41,7 +62,9 @@ mergedAsset: SELECT * FROM
|
||||
lae.checksum,
|
||||
NULL as owner_id,
|
||||
NULL as live_photo_video_id,
|
||||
lae.orientation
|
||||
lae.orientation,
|
||||
NULL as stack_id,
|
||||
0 AS stack_count
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
LEFT JOIN
|
||||
@@ -68,8 +91,16 @@ FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||
LEFT JOIN
|
||||
stack_entity se ON rae.stack_id = se.id
|
||||
WHERE
|
||||
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
|
||||
rae.deleted_at IS NULL
|
||||
AND rae.visibility = 0
|
||||
AND rae.owner_id in ?
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
OR rae.id = se.primary_asset_id
|
||||
)
|
||||
UNION ALL
|
||||
SELECT
|
||||
lae.name,
|
||||
|
||||
+15
-2
@@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
as i5;
|
||||
|
||||
class MergedAssetDrift extends i1.ModularAccessor {
|
||||
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||
@@ -18,7 +20,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
return customSelect(
|
||||
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, COALESCE(stack_count.total_count, 0) AS stack_count FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum LEFT JOIN stack_entity AS se ON rae.stack_id = se.id LEFT JOIN (SELECT stack_id, COUNT(*) AS total_count FROM remote_asset_entity WHERE deleted_at IS NULL AND visibility = 0 AND stack_id IS NOT NULL GROUP BY stack_id) AS stack_count ON rae.stack_id = stack_count.stack_id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, 0 AS stack_count FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
variables: [
|
||||
for (var $ in var1) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables
|
||||
@@ -26,6 +28,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
stackEntity,
|
||||
...generatedlimit.watchedTables,
|
||||
}).map((i0.QueryRow row) => MergedAssetResult(
|
||||
remoteId: row.readNullable<String>('remote_id'),
|
||||
@@ -44,6 +47,8 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
ownerId: row.readNullable<String>('owner_id'),
|
||||
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
|
||||
orientation: row.read<int>('orientation'),
|
||||
stackId: row.readNullable<String>('stack_id'),
|
||||
stackCount: row.read<int>('stack_count'),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -53,7 +58,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
||||
$arrayStartIndex += var2.length;
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||
variables: [
|
||||
i0.Variable<int>(groupBy),
|
||||
for (var $ in var2) i0.Variable<String>($)
|
||||
@@ -61,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
stackEntity,
|
||||
}).map((i0.QueryRow row) => MergedBucketResult(
|
||||
assetCount: row.read<int>('asset_count'),
|
||||
bucketDate: row.read<String>('bucket_date'),
|
||||
@@ -73,6 +79,9 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
i4.$LocalAssetEntityTable get localAssetEntity =>
|
||||
i1.ReadDatabaseContainer(attachedDatabase)
|
||||
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
|
||||
i5.$StackEntityTable get stackEntity =>
|
||||
i1.ReadDatabaseContainer(attachedDatabase)
|
||||
.resultSet<i5.$StackEntityTable>('stack_entity');
|
||||
}
|
||||
|
||||
class MergedAssetResult {
|
||||
@@ -91,6 +100,8 @@ class MergedAssetResult {
|
||||
final String? ownerId;
|
||||
final String? livePhotoVideoId;
|
||||
final int orientation;
|
||||
final String? stackId;
|
||||
final int stackCount;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
@@ -107,6 +118,8 @@ class MergedAssetResult {
|
||||
this.ownerId,
|
||||
this.livePhotoVideoId,
|
||||
required this.orientation,
|
||||
this.stackId,
|
||||
required this.stackCount,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class PersonEntity extends Table with DriftDefaultsMixin {
|
||||
const PersonEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
TextColumn get ownerId =>
|
||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get name => text()();
|
||||
|
||||
// TODO: foreign key refering to asset faces
|
||||
TextColumn get faceAssetId => text().nullable()();
|
||||
|
||||
TextColumn get thumbnailPath => text()();
|
||||
|
||||
BoolColumn get isFavorite => boolean()();
|
||||
|
||||
BoolColumn get isHidden => boolean()();
|
||||
|
||||
TextColumn get color => text().nullable()();
|
||||
|
||||
DateTimeColumn get birthDate => dateTime().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/person.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.dart' as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:drift/internal/modular.dart' as i5;
|
||||
|
||||
typedef $$PersonEntityTableCreateCompanionBuilder = i1.PersonEntityCompanion
|
||||
Function({
|
||||
required String id,
|
||||
i0.Value<DateTime> createdAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
required String ownerId,
|
||||
required String name,
|
||||
i0.Value<String?> faceAssetId,
|
||||
required String thumbnailPath,
|
||||
required bool isFavorite,
|
||||
required bool isHidden,
|
||||
i0.Value<String?> color,
|
||||
i0.Value<DateTime?> birthDate,
|
||||
});
|
||||
typedef $$PersonEntityTableUpdateCompanionBuilder = i1.PersonEntityCompanion
|
||||
Function({
|
||||
i0.Value<String> id,
|
||||
i0.Value<DateTime> createdAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<String> ownerId,
|
||||
i0.Value<String> name,
|
||||
i0.Value<String?> faceAssetId,
|
||||
i0.Value<String> thumbnailPath,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<bool> isHidden,
|
||||
i0.Value<String?> color,
|
||||
i0.Value<DateTime?> birthDate,
|
||||
});
|
||||
|
||||
final class $$PersonEntityTableReferences extends i0.BaseReferences<
|
||||
i0.GeneratedDatabase, i1.$PersonEntityTable, i1.PersonEntityData> {
|
||||
$$PersonEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||
.createAlias(i0.$_aliasNameGenerator(
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$PersonEntityTable>('person_entity')
|
||||
.ownerId,
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||
.id));
|
||||
|
||||
i4.$$UserEntityTableProcessedTableManager get ownerId {
|
||||
final $_column = $_itemColumn<String>('owner_id')!;
|
||||
|
||||
final manager = i4
|
||||
.$$UserEntityTableTableManager(
|
||||
$_db,
|
||||
i5.ReadDatabaseContainer($_db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'))
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $$PersonEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||
$$PersonEntityTableFilterComposer({
|
||||
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<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get name => $composableBuilder(
|
||||
column: $table.name, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get faceAssetId => $composableBuilder(
|
||||
column: $table.faceAssetId,
|
||||
builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get thumbnailPath => $composableBuilder(
|
||||
column: $table.thumbnailPath,
|
||||
builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
|
||||
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<bool> get isHidden => $composableBuilder(
|
||||
column: $table.isHidden, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get color => $composableBuilder(
|
||||
column: $table.color, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<DateTime> get birthDate => $composableBuilder(
|
||||
column: $table.birthDate, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i4.$$UserEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$PersonEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||
$$PersonEntityTableOrderingComposer({
|
||||
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<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get name => $composableBuilder(
|
||||
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get faceAssetId => $composableBuilder(
|
||||
column: $table.faceAssetId,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get thumbnailPath => $composableBuilder(
|
||||
column: $table.thumbnailPath,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
||||
column: $table.isFavorite,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<bool> get isHidden => $composableBuilder(
|
||||
column: $table.isHidden, builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get color => $composableBuilder(
|
||||
column: $table.color, builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<DateTime> get birthDate => $composableBuilder(
|
||||
column: $table.birthDate,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i4.$$UserEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$PersonEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||
$$PersonEntityTableAnnotationComposer({
|
||||
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<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get name =>
|
||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get faceAssetId => $composableBuilder(
|
||||
column: $table.faceAssetId, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get thumbnailPath => $composableBuilder(
|
||||
column: $table.thumbnailPath, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
|
||||
column: $table.isFavorite, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<bool> get isHidden =>
|
||||
$composableBuilder(column: $table.isHidden, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get color =>
|
||||
$composableBuilder(column: $table.color, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get birthDate =>
|
||||
$composableBuilder(column: $table.birthDate, builder: (column) => column);
|
||||
|
||||
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i4.$$UserEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$PersonEntityTableTableManager extends i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$PersonEntityTable,
|
||||
i1.PersonEntityData,
|
||||
i1.$$PersonEntityTableFilterComposer,
|
||||
i1.$$PersonEntityTableOrderingComposer,
|
||||
i1.$$PersonEntityTableAnnotationComposer,
|
||||
$$PersonEntityTableCreateCompanionBuilder,
|
||||
$$PersonEntityTableUpdateCompanionBuilder,
|
||||
(i1.PersonEntityData, i1.$$PersonEntityTableReferences),
|
||||
i1.PersonEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId})> {
|
||||
$$PersonEntityTableTableManager(
|
||||
i0.GeneratedDatabase db, i1.$PersonEntityTable table)
|
||||
: super(i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$PersonEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$PersonEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
i1.$$PersonEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
i0.Value<String> ownerId = const i0.Value.absent(),
|
||||
i0.Value<String> name = const i0.Value.absent(),
|
||||
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||
i0.Value<String> thumbnailPath = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<bool> isHidden = const i0.Value.absent(),
|
||||
i0.Value<String?> color = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> birthDate = const i0.Value.absent(),
|
||||
}) =>
|
||||
i1.PersonEntityCompanion(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
name: name,
|
||||
faceAssetId: faceAssetId,
|
||||
thumbnailPath: thumbnailPath,
|
||||
isFavorite: isFavorite,
|
||||
isHidden: isHidden,
|
||||
color: color,
|
||||
birthDate: birthDate,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String id,
|
||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
required String ownerId,
|
||||
required String name,
|
||||
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||
required String thumbnailPath,
|
||||
required bool isFavorite,
|
||||
required bool isHidden,
|
||||
i0.Value<String?> color = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> birthDate = const i0.Value.absent(),
|
||||
}) =>
|
||||
i1.PersonEntityCompanion.insert(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
name: name,
|
||||
faceAssetId: faceAssetId,
|
||||
thumbnailPath: thumbnailPath,
|
||||
isFavorite: isFavorite,
|
||||
isHidden: isHidden,
|
||||
color: color,
|
||||
birthDate: birthDate,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (
|
||||
e.readTable(table),
|
||||
i1.$$PersonEntityTableReferences(db, table, e)
|
||||
))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({ownerId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins: <
|
||||
T extends i0.TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic>>(state) {
|
||||
if (ownerId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.ownerId,
|
||||
referencedTable:
|
||||
i1.$$PersonEntityTableReferences._ownerIdTable(db),
|
||||
referencedColumn:
|
||||
i1.$$PersonEntityTableReferences._ownerIdTable(db).id,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
typedef $$PersonEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$PersonEntityTable,
|
||||
i1.PersonEntityData,
|
||||
i1.$$PersonEntityTableFilterComposer,
|
||||
i1.$$PersonEntityTableOrderingComposer,
|
||||
i1.$$PersonEntityTableAnnotationComposer,
|
||||
$$PersonEntityTableCreateCompanionBuilder,
|
||||
$$PersonEntityTableUpdateCompanionBuilder,
|
||||
(i1.PersonEntityData, i1.$$PersonEntityTableReferences),
|
||||
i1.PersonEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId})>;
|
||||
|
||||
class $PersonEntityTable extends i2.PersonEntity
|
||||
with i0.TableInfo<$PersonEntityTable, i1.PersonEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$PersonEntityTable(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 _createdAtMeta =
|
||||
const i0.VerificationMeta('createdAt');
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> createdAt =
|
||||
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i3.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);
|
||||
static const i0.VerificationMeta _ownerIdMeta =
|
||||
const i0.VerificationMeta('ownerId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
||||
'owner_id', aliasedName, false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||
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 _faceAssetIdMeta =
|
||||
const i0.VerificationMeta('faceAssetId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> faceAssetId =
|
||||
i0.GeneratedColumn<String>('face_asset_id', aliasedName, true,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||
static const i0.VerificationMeta _thumbnailPathMeta =
|
||||
const i0.VerificationMeta('thumbnailPath');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> thumbnailPath =
|
||||
i0.GeneratedColumn<String>('thumbnail_path', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _isFavoriteMeta =
|
||||
const i0.VerificationMeta('isFavorite');
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isFavorite = i0.GeneratedColumn<bool>(
|
||||
'is_favorite', aliasedName, false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_favorite" IN (0, 1))'));
|
||||
static const i0.VerificationMeta _isHiddenMeta =
|
||||
const i0.VerificationMeta('isHidden');
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> isHidden = i0.GeneratedColumn<bool>(
|
||||
'is_hidden', aliasedName, false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_hidden" IN (0, 1))'));
|
||||
static const i0.VerificationMeta _colorMeta =
|
||||
const i0.VerificationMeta('color');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> color = i0.GeneratedColumn<String>(
|
||||
'color', aliasedName, true,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||
static const i0.VerificationMeta _birthDateMeta =
|
||||
const i0.VerificationMeta('birthDate');
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> birthDate =
|
||||
i0.GeneratedColumn<DateTime>('birth_date', aliasedName, true,
|
||||
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
ownerId,
|
||||
name,
|
||||
faceAssetId,
|
||||
thumbnailPath,
|
||||
isFavorite,
|
||||
isHidden,
|
||||
color,
|
||||
birthDate
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'person_entity';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.PersonEntityData> 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('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
||||
}
|
||||
if (data.containsKey('owner_id')) {
|
||||
context.handle(_ownerIdMeta,
|
||||
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_ownerIdMeta);
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
if (data.containsKey('face_asset_id')) {
|
||||
context.handle(
|
||||
_faceAssetIdMeta,
|
||||
faceAssetId.isAcceptableOrUnknown(
|
||||
data['face_asset_id']!, _faceAssetIdMeta));
|
||||
}
|
||||
if (data.containsKey('thumbnail_path')) {
|
||||
context.handle(
|
||||
_thumbnailPathMeta,
|
||||
thumbnailPath.isAcceptableOrUnknown(
|
||||
data['thumbnail_path']!, _thumbnailPathMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_thumbnailPathMeta);
|
||||
}
|
||||
if (data.containsKey('is_favorite')) {
|
||||
context.handle(
|
||||
_isFavoriteMeta,
|
||||
isFavorite.isAcceptableOrUnknown(
|
||||
data['is_favorite']!, _isFavoriteMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_isFavoriteMeta);
|
||||
}
|
||||
if (data.containsKey('is_hidden')) {
|
||||
context.handle(_isHiddenMeta,
|
||||
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_isHiddenMeta);
|
||||
}
|
||||
if (data.containsKey('color')) {
|
||||
context.handle(
|
||||
_colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta));
|
||||
}
|
||||
if (data.containsKey('birth_date')) {
|
||||
context.handle(_birthDateMeta,
|
||||
birthDate.isAcceptableOrUnknown(data['birth_date']!, _birthDateMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.PersonEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.PersonEntityData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
||||
ownerId: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
||||
name: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
faceAssetId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string, data['${effectivePrefix}face_asset_id']),
|
||||
thumbnailPath: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!,
|
||||
isFavorite: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
|
||||
isHidden: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!,
|
||||
color: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}color']),
|
||||
birthDate: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}birth_date']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$PersonEntityTable createAlias(String alias) {
|
||||
return $PersonEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class PersonEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.PersonEntityData> {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String ownerId;
|
||||
final String name;
|
||||
final String? faceAssetId;
|
||||
final String thumbnailPath;
|
||||
final bool isFavorite;
|
||||
final bool isHidden;
|
||||
final String? color;
|
||||
final DateTime? birthDate;
|
||||
const PersonEntityData(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.ownerId,
|
||||
required this.name,
|
||||
this.faceAssetId,
|
||||
required this.thumbnailPath,
|
||||
required this.isFavorite,
|
||||
required this.isHidden,
|
||||
this.color,
|
||||
this.birthDate});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||
map['name'] = i0.Variable<String>(name);
|
||||
if (!nullToAbsent || faceAssetId != null) {
|
||||
map['face_asset_id'] = i0.Variable<String>(faceAssetId);
|
||||
}
|
||||
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath);
|
||||
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||
map['is_hidden'] = i0.Variable<bool>(isHidden);
|
||||
if (!nullToAbsent || color != null) {
|
||||
map['color'] = i0.Variable<String>(color);
|
||||
}
|
||||
if (!nullToAbsent || birthDate != null) {
|
||||
map['birth_date'] = i0.Variable<DateTime>(birthDate);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory PersonEntityData.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return PersonEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
faceAssetId: serializer.fromJson<String?>(json['faceAssetId']),
|
||||
thumbnailPath: serializer.fromJson<String>(json['thumbnailPath']),
|
||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||
isHidden: serializer.fromJson<bool>(json['isHidden']),
|
||||
color: serializer.fromJson<String?>(json['color']),
|
||||
birthDate: serializer.fromJson<DateTime?>(json['birthDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'ownerId': serializer.toJson<String>(ownerId),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'faceAssetId': serializer.toJson<String?>(faceAssetId),
|
||||
'thumbnailPath': serializer.toJson<String>(thumbnailPath),
|
||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||
'isHidden': serializer.toJson<bool>(isHidden),
|
||||
'color': serializer.toJson<String?>(color),
|
||||
'birthDate': serializer.toJson<DateTime?>(birthDate),
|
||||
};
|
||||
}
|
||||
|
||||
i1.PersonEntityData copyWith(
|
||||
{String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? ownerId,
|
||||
String? name,
|
||||
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||
String? thumbnailPath,
|
||||
bool? isFavorite,
|
||||
bool? isHidden,
|
||||
i0.Value<String?> color = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> birthDate = const i0.Value.absent()}) =>
|
||||
i1.PersonEntityData(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
name: name ?? this.name,
|
||||
faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId,
|
||||
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
color: color.present ? color.value : this.color,
|
||||
birthDate: birthDate.present ? birthDate.value : this.birthDate,
|
||||
);
|
||||
PersonEntityData copyWithCompanion(i1.PersonEntityCompanion data) {
|
||||
return PersonEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||
name: data.name.present ? data.name.value : this.name,
|
||||
faceAssetId:
|
||||
data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId,
|
||||
thumbnailPath: data.thumbnailPath.present
|
||||
? data.thumbnailPath.value
|
||||
: this.thumbnailPath,
|
||||
isFavorite:
|
||||
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
|
||||
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
|
||||
color: data.color.present ? data.color.value : this.color,
|
||||
birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('PersonEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('ownerId: $ownerId, ')
|
||||
..write('name: $name, ')
|
||||
..write('faceAssetId: $faceAssetId, ')
|
||||
..write('thumbnailPath: $thumbnailPath, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('isHidden: $isHidden, ')
|
||||
..write('color: $color, ')
|
||||
..write('birthDate: $birthDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name,
|
||||
faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.PersonEntityData &&
|
||||
other.id == this.id &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.ownerId == this.ownerId &&
|
||||
other.name == this.name &&
|
||||
other.faceAssetId == this.faceAssetId &&
|
||||
other.thumbnailPath == this.thumbnailPath &&
|
||||
other.isFavorite == this.isFavorite &&
|
||||
other.isHidden == this.isHidden &&
|
||||
other.color == this.color &&
|
||||
other.birthDate == this.birthDate);
|
||||
}
|
||||
|
||||
class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<DateTime> createdAt;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
final i0.Value<String> ownerId;
|
||||
final i0.Value<String> name;
|
||||
final i0.Value<String?> faceAssetId;
|
||||
final i0.Value<String> thumbnailPath;
|
||||
final i0.Value<bool> isFavorite;
|
||||
final i0.Value<bool> isHidden;
|
||||
final i0.Value<String?> color;
|
||||
final i0.Value<DateTime?> birthDate;
|
||||
const PersonEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
this.ownerId = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
this.faceAssetId = const i0.Value.absent(),
|
||||
this.thumbnailPath = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.isHidden = const i0.Value.absent(),
|
||||
this.color = const i0.Value.absent(),
|
||||
this.birthDate = const i0.Value.absent(),
|
||||
});
|
||||
PersonEntityCompanion.insert({
|
||||
required String id,
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
required String ownerId,
|
||||
required String name,
|
||||
this.faceAssetId = const i0.Value.absent(),
|
||||
required String thumbnailPath,
|
||||
required bool isFavorite,
|
||||
required bool isHidden,
|
||||
this.color = const i0.Value.absent(),
|
||||
this.birthDate = const i0.Value.absent(),
|
||||
}) : id = i0.Value(id),
|
||||
ownerId = i0.Value(ownerId),
|
||||
name = i0.Value(name),
|
||||
thumbnailPath = i0.Value(thumbnailPath),
|
||||
isFavorite = i0.Value(isFavorite),
|
||||
isHidden = i0.Value(isHidden);
|
||||
static i0.Insertable<i1.PersonEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<DateTime>? createdAt,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
i0.Expression<String>? ownerId,
|
||||
i0.Expression<String>? name,
|
||||
i0.Expression<String>? faceAssetId,
|
||||
i0.Expression<String>? thumbnailPath,
|
||||
i0.Expression<bool>? isFavorite,
|
||||
i0.Expression<bool>? isHidden,
|
||||
i0.Expression<String>? color,
|
||||
i0.Expression<DateTime>? birthDate,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (ownerId != null) 'owner_id': ownerId,
|
||||
if (name != null) 'name': name,
|
||||
if (faceAssetId != null) 'face_asset_id': faceAssetId,
|
||||
if (thumbnailPath != null) 'thumbnail_path': thumbnailPath,
|
||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||
if (isHidden != null) 'is_hidden': isHidden,
|
||||
if (color != null) 'color': color,
|
||||
if (birthDate != null) 'birth_date': birthDate,
|
||||
});
|
||||
}
|
||||
|
||||
i1.PersonEntityCompanion copyWith(
|
||||
{i0.Value<String>? id,
|
||||
i0.Value<DateTime>? createdAt,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
i0.Value<String>? ownerId,
|
||||
i0.Value<String>? name,
|
||||
i0.Value<String?>? faceAssetId,
|
||||
i0.Value<String>? thumbnailPath,
|
||||
i0.Value<bool>? isFavorite,
|
||||
i0.Value<bool>? isHidden,
|
||||
i0.Value<String?>? color,
|
||||
i0.Value<DateTime?>? birthDate}) {
|
||||
return i1.PersonEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
name: name ?? this.name,
|
||||
faceAssetId: faceAssetId ?? this.faceAssetId,
|
||||
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
color: color ?? this.color,
|
||||
birthDate: birthDate ?? this.birthDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<String>(id.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
if (ownerId.present) {
|
||||
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
if (faceAssetId.present) {
|
||||
map['face_asset_id'] = i0.Variable<String>(faceAssetId.value);
|
||||
}
|
||||
if (thumbnailPath.present) {
|
||||
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath.value);
|
||||
}
|
||||
if (isFavorite.present) {
|
||||
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
|
||||
}
|
||||
if (isHidden.present) {
|
||||
map['is_hidden'] = i0.Variable<bool>(isHidden.value);
|
||||
}
|
||||
if (color.present) {
|
||||
map['color'] = i0.Variable<String>(color.value);
|
||||
}
|
||||
if (birthDate.present) {
|
||||
map['birth_date'] = i0.Variable<DateTime>(birthDate.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('PersonEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('ownerId: $ownerId, ')
|
||||
..write('name: $name, ')
|
||||
..write('faceAssetId: $faceAssetId, ')
|
||||
..write('thumbnailPath: $thumbnailPath, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('isHidden: $isHidden, ')
|
||||
..write('color: $color, ')
|
||||
..write('birthDate: $birthDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ class RemoteAssetEntity extends Table
|
||||
|
||||
IntColumn get visibility => intEnum<AssetVisibility>()();
|
||||
|
||||
TextColumn get stackId => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -55,5 +57,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
visibility: visibility,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
localId: null,
|
||||
stackId: stackId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
|
||||
i0.Value<DateTime?> deletedAt,
|
||||
i0.Value<String?> livePhotoVideoId,
|
||||
required i2.AssetVisibility visibility,
|
||||
i0.Value<String?> stackId,
|
||||
});
|
||||
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
||||
= i1.RemoteAssetEntityCompanion Function({
|
||||
@@ -48,6 +49,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
||||
i0.Value<DateTime?> deletedAt,
|
||||
i0.Value<String?> livePhotoVideoId,
|
||||
i0.Value<i2.AssetVisibility> visibility,
|
||||
i0.Value<String?> stackId,
|
||||
});
|
||||
|
||||
final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences<
|
||||
@@ -145,6 +147,9 @@ class $$RemoteAssetEntityTableFilterComposer
|
||||
column: $table.visibility,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get stackId => $composableBuilder(
|
||||
column: $table.stackId, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -231,6 +236,9 @@ class $$RemoteAssetEntityTableOrderingComposer
|
||||
column: $table.visibility,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get stackId => $composableBuilder(
|
||||
column: $table.stackId, builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -309,6 +317,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
|
||||
$composableBuilder(
|
||||
column: $table.visibility, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get stackId =>
|
||||
$composableBuilder(column: $table.stackId, builder: (column) => column);
|
||||
|
||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -373,6 +384,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
|
||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||
}) =>
|
||||
i1.RemoteAssetEntityCompanion(
|
||||
name: name,
|
||||
@@ -391,6 +403,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
||||
deletedAt: deletedAt,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String name,
|
||||
@@ -409,6 +422,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||
required i2.AssetVisibility visibility,
|
||||
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||
}) =>
|
||||
i1.RemoteAssetEntityCompanion.insert(
|
||||
name: name,
|
||||
@@ -427,6 +441,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
||||
deletedAt: deletedAt,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (
|
||||
@@ -602,6 +617,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
||||
.withConverter<i2.AssetVisibility>(
|
||||
i1.$RemoteAssetEntityTable.$convertervisibility);
|
||||
static const i0.VerificationMeta _stackIdMeta =
|
||||
const i0.VerificationMeta('stackId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> stackId = i0.GeneratedColumn<String>(
|
||||
'stack_id', aliasedName, true,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
name,
|
||||
@@ -619,7 +640,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
thumbHash,
|
||||
deletedAt,
|
||||
livePhotoVideoId,
|
||||
visibility
|
||||
visibility,
|
||||
stackId
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -703,6 +725,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
livePhotoVideoId.isAcceptableOrUnknown(
|
||||
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
|
||||
}
|
||||
if (data.containsKey('stack_id')) {
|
||||
context.handle(_stackIdMeta,
|
||||
stackId.isAcceptableOrUnknown(data['stack_id']!, _stackIdMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -748,6 +774,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
|
||||
stackId: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}stack_id']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -785,6 +813,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
final DateTime? deletedAt;
|
||||
final String? livePhotoVideoId;
|
||||
final i2.AssetVisibility visibility;
|
||||
final String? stackId;
|
||||
const RemoteAssetEntityData(
|
||||
{required this.name,
|
||||
required this.type,
|
||||
@@ -801,7 +830,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
this.thumbHash,
|
||||
this.deletedAt,
|
||||
this.livePhotoVideoId,
|
||||
required this.visibility});
|
||||
required this.visibility,
|
||||
this.stackId});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
@@ -841,6 +871,9 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
map['visibility'] = i0.Variable<int>(
|
||||
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
|
||||
}
|
||||
if (!nullToAbsent || stackId != null) {
|
||||
map['stack_id'] = i0.Variable<String>(stackId);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -866,6 +899,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
|
||||
visibility: i1.$RemoteAssetEntityTable.$convertervisibility
|
||||
.fromJson(serializer.fromJson<int>(json['visibility'])),
|
||||
stackId: serializer.fromJson<String?>(json['stackId']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -890,6 +924,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
|
||||
'visibility': serializer.toJson<int>(
|
||||
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
|
||||
'stackId': serializer.toJson<String?>(stackId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -909,7 +944,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
i0.Value<String?> thumbHash = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||
i2.AssetVisibility? visibility}) =>
|
||||
i2.AssetVisibility? visibility,
|
||||
i0.Value<String?> stackId = const i0.Value.absent()}) =>
|
||||
i1.RemoteAssetEntityData(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -932,6 +968,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
? livePhotoVideoId.value
|
||||
: this.livePhotoVideoId,
|
||||
visibility: visibility ?? this.visibility,
|
||||
stackId: stackId.present ? stackId.value : this.stackId,
|
||||
);
|
||||
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
||||
return RemoteAssetEntityData(
|
||||
@@ -959,6 +996,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
: this.livePhotoVideoId,
|
||||
visibility:
|
||||
data.visibility.present ? data.visibility.value : this.visibility,
|
||||
stackId: data.stackId.present ? data.stackId.value : this.stackId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -980,7 +1018,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
..write('thumbHash: $thumbHash, ')
|
||||
..write('deletedAt: $deletedAt, ')
|
||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||
..write('visibility: $visibility')
|
||||
..write('visibility: $visibility, ')
|
||||
..write('stackId: $stackId')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1002,7 +1041,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
thumbHash,
|
||||
deletedAt,
|
||||
livePhotoVideoId,
|
||||
visibility);
|
||||
visibility,
|
||||
stackId);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@@ -1022,7 +1062,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
||||
other.thumbHash == this.thumbHash &&
|
||||
other.deletedAt == this.deletedAt &&
|
||||
other.livePhotoVideoId == this.livePhotoVideoId &&
|
||||
other.visibility == this.visibility);
|
||||
other.visibility == this.visibility &&
|
||||
other.stackId == this.stackId);
|
||||
}
|
||||
|
||||
class RemoteAssetEntityCompanion
|
||||
@@ -1043,6 +1084,7 @@ class RemoteAssetEntityCompanion
|
||||
final i0.Value<DateTime?> deletedAt;
|
||||
final i0.Value<String?> livePhotoVideoId;
|
||||
final i0.Value<i2.AssetVisibility> visibility;
|
||||
final i0.Value<String?> stackId;
|
||||
const RemoteAssetEntityCompanion({
|
||||
this.name = const i0.Value.absent(),
|
||||
this.type = const i0.Value.absent(),
|
||||
@@ -1060,6 +1102,7 @@ class RemoteAssetEntityCompanion
|
||||
this.deletedAt = const i0.Value.absent(),
|
||||
this.livePhotoVideoId = const i0.Value.absent(),
|
||||
this.visibility = const i0.Value.absent(),
|
||||
this.stackId = const i0.Value.absent(),
|
||||
});
|
||||
RemoteAssetEntityCompanion.insert({
|
||||
required String name,
|
||||
@@ -1078,6 +1121,7 @@ class RemoteAssetEntityCompanion
|
||||
this.deletedAt = const i0.Value.absent(),
|
||||
this.livePhotoVideoId = const i0.Value.absent(),
|
||||
required i2.AssetVisibility visibility,
|
||||
this.stackId = const i0.Value.absent(),
|
||||
}) : name = i0.Value(name),
|
||||
type = i0.Value(type),
|
||||
id = i0.Value(id),
|
||||
@@ -1101,6 +1145,7 @@ class RemoteAssetEntityCompanion
|
||||
i0.Expression<DateTime>? deletedAt,
|
||||
i0.Expression<String>? livePhotoVideoId,
|
||||
i0.Expression<int>? visibility,
|
||||
i0.Expression<String>? stackId,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (name != null) 'name': name,
|
||||
@@ -1119,6 +1164,7 @@ class RemoteAssetEntityCompanion
|
||||
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
|
||||
if (visibility != null) 'visibility': visibility,
|
||||
if (stackId != null) 'stack_id': stackId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1138,7 +1184,8 @@ class RemoteAssetEntityCompanion
|
||||
i0.Value<String?>? thumbHash,
|
||||
i0.Value<DateTime?>? deletedAt,
|
||||
i0.Value<String?>? livePhotoVideoId,
|
||||
i0.Value<i2.AssetVisibility>? visibility}) {
|
||||
i0.Value<i2.AssetVisibility>? visibility,
|
||||
i0.Value<String?>? stackId}) {
|
||||
return i1.RemoteAssetEntityCompanion(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -1156,6 +1203,7 @@ class RemoteAssetEntityCompanion
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
visibility: visibility ?? this.visibility,
|
||||
stackId: stackId ?? this.stackId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1213,6 +1261,9 @@ class RemoteAssetEntityCompanion
|
||||
.$RemoteAssetEntityTable.$convertervisibility
|
||||
.toSql(visibility.value));
|
||||
}
|
||||
if (stackId.present) {
|
||||
map['stack_id'] = i0.Variable<String>(stackId.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1234,7 +1285,8 @@ class RemoteAssetEntityCompanion
|
||||
..write('thumbHash: $thumbHash, ')
|
||||
..write('deletedAt: $deletedAt, ')
|
||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||
..write('visibility: $visibility')
|
||||
..write('visibility: $visibility, ')
|
||||
..write('stackId: $stackId')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@@ -15,7 +14,7 @@ class StackEntity extends Table with DriftDefaultsMixin {
|
||||
TextColumn get ownerId =>
|
||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
|
||||
TextColumn get primaryAssetId => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
||||
+16
-119
@@ -8,8 +8,6 @@ import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:drift/internal/modular.dart' as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i6;
|
||||
|
||||
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
|
||||
Function({
|
||||
@@ -57,33 +55,6 @@ final class $$StackEntityTableReferences extends i0.BaseReferences<
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
|
||||
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
|
||||
i0.GeneratedDatabase db) =>
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(i0.$_aliasNameGenerator(
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$StackEntityTable>('stack_entity')
|
||||
.primaryAssetId,
|
||||
i5.ReadDatabaseContainer(db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.id));
|
||||
|
||||
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
|
||||
final $_column = $_itemColumn<String>('primary_asset_id')!;
|
||||
|
||||
final manager = i6
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i5.ReadDatabaseContainer($_db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $$StackEntityTableFilterComposer
|
||||
@@ -104,6 +75,10 @@ class $$StackEntityTableFilterComposer
|
||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i0.ColumnFilters<String> get primaryAssetId => $composableBuilder(
|
||||
column: $table.primaryAssetId,
|
||||
builder: (column) => i0.ColumnFilters(column));
|
||||
|
||||
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -125,28 +100,6 @@ class $$StackEntityTableFilterComposer
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
|
||||
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.primaryAssetId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i6.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$StackEntityTableOrderingComposer
|
||||
@@ -169,6 +122,10 @@ class $$StackEntityTableOrderingComposer
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i0.ColumnOrderings<String> get primaryAssetId => $composableBuilder(
|
||||
column: $table.primaryAssetId,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -190,30 +147,6 @@ class $$StackEntityTableOrderingComposer
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
|
||||
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.primaryAssetId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i6.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||
'remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$StackEntityTableAnnotationComposer
|
||||
@@ -234,6 +167,9 @@ class $$StackEntityTableAnnotationComposer
|
||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get primaryAssetId => $composableBuilder(
|
||||
column: $table.primaryAssetId, builder: (column) => column);
|
||||
|
||||
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@@ -255,30 +191,6 @@ class $$StackEntityTableAnnotationComposer
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
|
||||
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
|
||||
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.primaryAssetId,
|
||||
referencedTable: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i6.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i5.ReadDatabaseContainer($db)
|
||||
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||
'remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||
@@ -292,7 +204,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||
$$StackEntityTableUpdateCompanionBuilder,
|
||||
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||
i1.StackEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
|
||||
i0.PrefetchHooks Function({bool ownerId})> {
|
||||
$$StackEntityTableTableManager(
|
||||
i0.GeneratedDatabase db, i1.$StackEntityTable table)
|
||||
: super(i0.TableManagerState(
|
||||
@@ -338,7 +250,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||
i1.$$StackEntityTableReferences(db, table, e)
|
||||
))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) {
|
||||
prefetchHooksCallback: ({ownerId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
@@ -365,17 +277,6 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
|
||||
) as T;
|
||||
}
|
||||
if (primaryAssetId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.primaryAssetId,
|
||||
referencedTable: i1.$$StackEntityTableReferences
|
||||
._primaryAssetIdTable(db),
|
||||
referencedColumn: i1.$$StackEntityTableReferences
|
||||
._primaryAssetIdTable(db)
|
||||
.id,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
@@ -398,7 +299,7 @@ typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||
$$StackEntityTableUpdateCompanionBuilder,
|
||||
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||
i1.StackEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
|
||||
i0.PrefetchHooks Function({bool ownerId})>;
|
||||
|
||||
class $StackEntityTable extends i2.StackEntity
|
||||
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
|
||||
@@ -440,12 +341,8 @@ class $StackEntityTable extends i2.StackEntity
|
||||
const i0.VerificationMeta('primaryAssetId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> primaryAssetId =
|
||||
i0.GeneratedColumn<String>(
|
||||
'primary_asset_id', aliasedName, false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_asset_entity (id)'));
|
||||
i0.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns =>
|
||||
[id, createdAt, updatedAt, ownerId, primaryAssetId];
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import "package:immich_mobile/utils/database.utils.dart";
|
||||
|
||||
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
||||
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
||||
class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftBackupRepository(this._db) : super(_db);
|
||||
|
||||
_getExcludedSubquery() {
|
||||
return _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.localAlbumEntity.backupSelection
|
||||
.equalsValue(BackupSelection.excluded),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> getTotalCount() async {
|
||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.localAlbumEntity.backupSelection
|
||||
.equalsValue(BackupSelection.selected) &
|
||||
_db.localAlbumAssetEntity.assetId
|
||||
.isNotInQuery(_getExcludedSubquery()),
|
||||
);
|
||||
|
||||
return query.get().then((rows) => rows.length);
|
||||
}
|
||||
|
||||
Future<int> getRemainderCount() async {
|
||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.localAssetEntity,
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum
|
||||
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.localAlbumEntity.backupSelection
|
||||
.equalsValue(BackupSelection.selected) &
|
||||
_db.remoteAssetEntity.id.isNull() &
|
||||
_db.localAlbumAssetEntity.assetId
|
||||
.isNotInQuery(_getExcludedSubquery()),
|
||||
);
|
||||
|
||||
return query.get().then((rows) => rows.length);
|
||||
}
|
||||
|
||||
Future<int> getBackupCount() async {
|
||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||
..addColumns(
|
||||
[_db.localAlbumAssetEntity.assetId],
|
||||
)
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.localAssetEntity,
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum
|
||||
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.localAlbumEntity.backupSelection
|
||||
.equalsValue(BackupSelection.selected) &
|
||||
_db.remoteAssetEntity.id.isNotNull() &
|
||||
_db.localAlbumAssetEntity.assetId
|
||||
.isNotInQuery(_getExcludedSubquery()),
|
||||
);
|
||||
|
||||
return query.get().then((rows) => rows.length);
|
||||
}
|
||||
|
||||
Future<List<LocalAsset>> getCandidates() async {
|
||||
final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true)
|
||||
..addColumns([_db.localAlbumEntity.id])
|
||||
..where(
|
||||
_db.localAlbumEntity.backupSelection
|
||||
.equalsValue(BackupSelection.selected),
|
||||
);
|
||||
|
||||
final query = _db.localAssetEntity.select()
|
||||
..where(
|
||||
(lae) =>
|
||||
existsQuery(
|
||||
_db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..where(
|
||||
_db.localAlbumAssetEntity.albumId
|
||||
.isInQuery(selectedAlbumIds) &
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
|
||||
),
|
||||
) &
|
||||
notExistsQuery(
|
||||
_db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([_db.remoteAssetEntity.checksum])
|
||||
..where(
|
||||
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) &
|
||||
lae.checksum.isNotNull(),
|
||||
),
|
||||
) &
|
||||
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||
)
|
||||
..orderBy(
|
||||
[
|
||||
(localAsset) => OrderingTerm.desc(localAsset.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
return query.map((localAsset) => localAsset.toDto()).get();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
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/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||
@@ -17,6 +19,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import 'db.repository.drift.dart';
|
||||
@@ -52,6 +55,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
MemoryEntity,
|
||||
MemoryAssetEntity,
|
||||
StackEntity,
|
||||
PersonEntity,
|
||||
],
|
||||
include: {
|
||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||
@@ -68,10 +72,40 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 3;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Run migration steps without foreign keys and re-enable them later
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await m.runMigrationSteps(
|
||||
from: from,
|
||||
to: to,
|
||||
steps: migrationSteps(
|
||||
from1To2: (m, v2) async {
|
||||
for (final entity in v2.entities) {
|
||||
await m.drop(entity);
|
||||
await m.create(entity);
|
||||
}
|
||||
},
|
||||
from2To3: (m, v3) async {
|
||||
// Removed foreign key constraint on stack.primaryAssetId
|
||||
await m.alterTable(TableMigration(v3.stackEntity));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
// Fail if the migration broke foreign keys
|
||||
final wrongFKs =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
await customStatement('PRAGMA synchronous = NORMAL');
|
||||
|
||||
+79
-66
@@ -7,31 +7,33 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
as i10;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
as i11;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
as i10;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
as i11;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i14;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
as i15;
|
||||
import 'package:drift/internal/modular.dart' as i16;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i16;
|
||||
import 'package:drift/internal/modular.dart' as i17;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -41,28 +43,29 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i2.$RemoteAssetEntityTable(this);
|
||||
late final i3.$LocalAssetEntityTable localAssetEntity =
|
||||
i3.$LocalAssetEntityTable(this);
|
||||
late final i4.$UserMetadataEntityTable userMetadataEntity =
|
||||
i4.$UserMetadataEntityTable(this);
|
||||
late final i5.$PartnerEntityTable partnerEntity =
|
||||
i5.$PartnerEntityTable(this);
|
||||
late final i6.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i6.$LocalAlbumEntityTable(this);
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||
i7.$LocalAlbumAssetEntityTable(this);
|
||||
late final i8.$RemoteExifEntityTable remoteExifEntity =
|
||||
i8.$RemoteExifEntityTable(this);
|
||||
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
|
||||
i9.$RemoteAlbumEntityTable(this);
|
||||
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
|
||||
i10.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
|
||||
i11.$RemoteAlbumUserEntityTable(this);
|
||||
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
|
||||
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
|
||||
i13.$MemoryAssetEntityTable(this);
|
||||
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
|
||||
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
|
||||
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
|
||||
late final i4.$StackEntityTable stackEntity = i4.$StackEntityTable(this);
|
||||
late final i5.$UserMetadataEntityTable userMetadataEntity =
|
||||
i5.$UserMetadataEntityTable(this);
|
||||
late final i6.$PartnerEntityTable partnerEntity =
|
||||
i6.$PartnerEntityTable(this);
|
||||
late final i7.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i7.$LocalAlbumEntityTable(this);
|
||||
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||
i8.$LocalAlbumAssetEntityTable(this);
|
||||
late final i9.$RemoteExifEntityTable remoteExifEntity =
|
||||
i9.$RemoteExifEntityTable(this);
|
||||
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity =
|
||||
i10.$RemoteAlbumEntityTable(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);
|
||||
i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this)
|
||||
.accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -71,6 +74,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
stackEntity,
|
||||
i3.idxLocalAssetChecksum,
|
||||
i2.uQRemoteAssetOwnerChecksum,
|
||||
i2.idxRemoteAssetChecksum,
|
||||
@@ -84,7 +88,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
stackEntity
|
||||
personEntity
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||
@@ -97,6 +101,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
result: [
|
||||
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
@@ -213,7 +224,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
result: [
|
||||
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
|
||||
i0.TableUpdate('person_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -232,27 +243,29 @@ class $DriftManager {
|
||||
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i5.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
i4.$$StackEntityTableTableManager get stackEntity =>
|
||||
i4.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
i5.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i6.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i10.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
_db, _db.remoteAlbumAssetEntity);
|
||||
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
|
||||
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
|
||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||
i12.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i14.$$StackEntityTableTableManager get stackEntity =>
|
||||
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
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);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,16 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/database.utils.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum }
|
||||
enum SortLocalAlbumsBy {
|
||||
id,
|
||||
backupSelection,
|
||||
isIosSharedAlbum,
|
||||
name,
|
||||
assetCount
|
||||
}
|
||||
|
||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
@@ -40,6 +47,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
OrderingTerm.asc(_db.localAlbumEntity.backupSelection),
|
||||
SortLocalAlbumsBy.isIosSharedAlbum =>
|
||||
OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum),
|
||||
SortLocalAlbumsBy.name =>
|
||||
OrderingTerm.asc(_db.localAlbumEntity.name),
|
||||
SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -150,7 +160,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
batch.insert(
|
||||
_db.localAlbumEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
onConflict: DoUpdate(
|
||||
(old) => LocalAlbumEntityCompanion(
|
||||
id: companion.id,
|
||||
name: companion.name,
|
||||
updatedAt: companion.updatedAt,
|
||||
isIosSharedAlbum: companion.isIosSharedAlbum,
|
||||
marker_: companion.marker_,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -381,30 +399,3 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
}
|
||||
|
||||
extension on LocalAlbumEntityData {
|
||||
LocalAlbum toDto({int assetCount = 0}) {
|
||||
return LocalAlbum(
|
||||
id: id,
|
||||
name: name,
|
||||
updatedAt: updatedAt,
|
||||
assetCount: assetCount,
|
||||
backupSelection: backupSelection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on LocalAssetEntityData {
|
||||
LocalAsset toDto() {
|
||||
return LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: checksum,
|
||||
type: type,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
@@ -43,4 +44,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> delete(List<String> ids) {
|
||||
if (ids.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
return _db.batch((batch) {
|
||||
for (final slice in ids.slices(32000)) {
|
||||
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<LocalAsset?> getById(String id) {
|
||||
final query = _db.localAssetEntity.select()
|
||||
..where((lae) => lae.id.equals(id));
|
||||
|
||||
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class DriftPartnerRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftPartnerRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<PartnerUserDto>> getPartners(String userId) {
|
||||
final query = _db.select(_db.partnerEntity).join([
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.partnerEntity.sharedWithId.equals(userId),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
final user = row.readTable(_db.userEntity);
|
||||
final partner = row.readTable(_db.partnerEntity);
|
||||
return PartnerUserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
inTimeline: partner.inTimeline,
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
// Get users who we can share our library with
|
||||
Future<List<PartnerUserDto>> getAvailablePartners(String currentUserId) {
|
||||
final query = _db.select(_db.userEntity)
|
||||
..where((row) => row.id.equals(currentUserId).not());
|
||||
|
||||
return query.map((user) {
|
||||
return PartnerUserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
inTimeline: false,
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
// Get users who are sharing their photos WITH the current user
|
||||
Future<List<PartnerUserDto>> getSharedWith(String partnerId) {
|
||||
final query = _db.select(_db.partnerEntity).join([
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.partnerEntity.sharedWithId.equals(partnerId),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
final user = row.readTable(_db.userEntity);
|
||||
final partner = row.readTable(_db.partnerEntity);
|
||||
return PartnerUserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
inTimeline: partner.inTimeline,
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
// Get users who the current user is sharing their photos TO
|
||||
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||
final query = _db.select(_db.partnerEntity).join([
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.partnerEntity.sharedById.equals(userId),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
final user = row.readTable(_db.userEntity);
|
||||
final partner = row.readTable(_db.partnerEntity);
|
||||
return PartnerUserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
inTimeline: partner.inTimeline,
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
Future<List<String>> getAllPartnerIds(String userId) async {
|
||||
// Get users who are sharing with me (sharedWithId = userId)
|
||||
final sharingWithMeQuery = _db.select(_db.partnerEntity)
|
||||
..where((tbl) => tbl.sharedWithId.equals(userId));
|
||||
final sharingWithMe =
|
||||
await sharingWithMeQuery.map((row) => row.sharedById).get();
|
||||
|
||||
// Get users who I am sharing with (sharedById = userId)
|
||||
final sharingWithThemQuery = _db.select(_db.partnerEntity)
|
||||
..where((tbl) => tbl.sharedById.equals(userId));
|
||||
final sharingWithThem =
|
||||
await sharingWithThemQuery.map((row) => row.sharedWithId).get();
|
||||
|
||||
// Combine both lists and remove duplicates
|
||||
final allPartnerIds =
|
||||
<String>{...sharingWithMe, ...sharingWithThem}.toList();
|
||||
return allPartnerIds;
|
||||
}
|
||||
|
||||
Future<PartnerUserDto?> getPartner(String partnerId, String userId) {
|
||||
final query = _db.select(_db.partnerEntity).join([
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.partnerEntity.sharedById.equals(partnerId) &
|
||||
_db.partnerEntity.sharedWithId.equals(userId),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
final user = row.readTable(_db.userEntity);
|
||||
final partner = row.readTable(_db.partnerEntity);
|
||||
return PartnerUserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
inTimeline: partner.inTimeline,
|
||||
);
|
||||
}).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<bool> toggleShowInTimeline(PartnerUserDto partner, String userId) {
|
||||
return _db.partnerEntity.update().replace(
|
||||
PartnerEntityCompanion(
|
||||
sharedById: Value(partner.id),
|
||||
sharedWithId: Value(userId),
|
||||
inTimeline: Value(!partner.inTimeline),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> create(String partnerId, String userId) {
|
||||
final entity = PartnerEntityCompanion(
|
||||
sharedById: Value(userId),
|
||||
sharedWithId: Value(partnerId),
|
||||
inTimeline: const Value(false),
|
||||
);
|
||||
|
||||
return _db.partnerEntity.insertOne(entity);
|
||||
}
|
||||
|
||||
Future<void> delete(String partnerId, String userId) {
|
||||
return _db.partnerEntity.deleteWhere(
|
||||
(t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class DriftPersonRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftPersonRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<Person>> getAll(String userId) {
|
||||
final query = _db.personEntity.select()
|
||||
..where((e) => e.ownerId.equals(userId));
|
||||
|
||||
return query.map((person) {
|
||||
return person.toDto();
|
||||
}).get();
|
||||
}
|
||||
}
|
||||
|
||||
extension on PersonEntityData {
|
||||
Person toDto() {
|
||||
return Person(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
name: name,
|
||||
faceAssetId: faceAssetId,
|
||||
thumbnailPath: thumbnailPath,
|
||||
isFavorite: isFavorite,
|
||||
isHidden: isHidden,
|
||||
color: color,
|
||||
birthDate: birthDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||
@@ -99,11 +105,169 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> update(RemoteAlbum album) async {
|
||||
await _db.remoteAlbumEntity.update().replace(
|
||||
RemoteAlbumEntityCompanion(
|
||||
id: Value(album.id),
|
||||
name: Value(album.name),
|
||||
ownerId: Value(album.ownerId),
|
||||
createdAt: Value(album.createdAt),
|
||||
updatedAt: Value(album.updatedAt),
|
||||
description: Value(album.description),
|
||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||
isActivityEnabled: Value(album.isActivityEnabled),
|
||||
order: Value(album.order),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeAssets(String albumId, List<String> assetIds) {
|
||||
return _db.remoteAlbumAssetEntity.deleteWhere(
|
||||
(tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds),
|
||||
);
|
||||
}
|
||||
|
||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([
|
||||
_db.remoteAssetEntity.createdAt.min(),
|
||||
_db.remoteAssetEntity.createdAt.max(),
|
||||
])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.id
|
||||
.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
),
|
||||
]);
|
||||
|
||||
return query.map((row) {
|
||||
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
|
||||
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
|
||||
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
|
||||
}).getSingle();
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getSharedUsers(String albumId) async {
|
||||
final albumUserRows = await (_db.select(_db.remoteAlbumUserEntity)
|
||||
..where((row) => row.albumId.equals(albumId)))
|
||||
.get();
|
||||
|
||||
if (albumUserRows.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final userIds = albumUserRows.map((row) => row.userId);
|
||||
|
||||
return (_db.select(_db.userEntity)..where((row) => row.id.isIn(userIds)))
|
||||
.map(
|
||||
(user) => UserDto(
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
profileImagePath: user.profileImagePath?.isEmpty == true
|
||||
? null
|
||||
: user.profileImagePath,
|
||||
isAdmin: user.isAdmin,
|
||||
updatedAt: user.updatedAt,
|
||||
quotaSizeInBytes: user.quotaSizeInBytes ?? 0,
|
||||
quotaUsageInBytes: user.quotaUsageInBytes,
|
||||
memoryEnabled: true,
|
||||
inTimeline: false,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.select().join([
|
||||
innerJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId));
|
||||
|
||||
return query
|
||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
||||
final albumAssets = assetIds.map(
|
||||
(assetId) => RemoteAlbumAssetEntityCompanion(
|
||||
albumId: Value(albumId),
|
||||
assetId: Value(assetId),
|
||||
),
|
||||
);
|
||||
|
||||
await _db.batch((batch) {
|
||||
batch.insertAll(
|
||||
_db.remoteAlbumAssetEntity,
|
||||
albumAssets,
|
||||
);
|
||||
});
|
||||
|
||||
return assetIds.length;
|
||||
}
|
||||
|
||||
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||
final albumUsers = userIds.map(
|
||||
(assetId) => RemoteAlbumUserEntityCompanion(
|
||||
albumId: Value(albumId),
|
||||
userId: Value(assetId),
|
||||
role: const Value(AlbumUserRole.editor),
|
||||
),
|
||||
);
|
||||
|
||||
return _db.batch((batch) {
|
||||
batch.insertAll(
|
||||
_db.remoteAlbumUserEntity,
|
||||
albumUsers,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) async {
|
||||
return _db.transaction(() async {
|
||||
await _db.remoteAlbumEntity.deleteWhere(
|
||||
(table) => table.id.equals(albumId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||
final query = _db.remoteAlbumEntity.select().join([
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumAssetEntity,
|
||||
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
||||
..addColumns([_db.userEntity.name])
|
||||
..groupBy([_db.remoteAlbumEntity.id]);
|
||||
|
||||
return query.map((row) {
|
||||
final album = row.readTable(_db.remoteAlbumEntity).toDto(
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
);
|
||||
return album;
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
extension on RemoteAlbumEntityData {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:drift/drift.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/stack.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||
hide ExifInfo;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@@ -30,25 +32,66 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Stream<RemoteAsset?> watchAsset(String id) {
|
||||
final query = _db.remoteAssetEntity
|
||||
.select()
|
||||
.addColumns([_db.localAssetEntity.id]).join([
|
||||
final stackCountRef = _db.stackEntity.id.count();
|
||||
|
||||
final query = _db.remoteAssetEntity.select().addColumns([
|
||||
_db.localAssetEntity.id,
|
||||
_db.stackEntity.primaryAssetId,
|
||||
stackCountRef,
|
||||
]).join([
|
||||
leftOuterJoin(
|
||||
_db.localAssetEntity,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.stackEntity,
|
||||
_db.stackEntity.primaryAssetId.equalsExp(_db.remoteAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity.createAlias('stacked_assets'),
|
||||
_db.stackEntity.id.equalsExp(
|
||||
_db.remoteAssetEntity.createAlias('stacked_assets').stackId,
|
||||
),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAssetEntity.id.equals(id));
|
||||
..where(_db.remoteAssetEntity.id.equals(id))
|
||||
..groupBy([
|
||||
_db.remoteAssetEntity.id,
|
||||
_db.localAssetEntity.id,
|
||||
_db.stackEntity.primaryAssetId,
|
||||
]);
|
||||
|
||||
return query.map((row) {
|
||||
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
||||
final primaryAssetId = row.read(_db.stackEntity.primaryAssetId);
|
||||
final stackCount =
|
||||
primaryAssetId == id ? (row.read(stackCountRef) ?? 0) : 0;
|
||||
|
||||
return asset.copyWith(
|
||||
localId: row.read(_db.localAssetEntity.id),
|
||||
stackCount: stackCount,
|
||||
);
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||
if (asset.stackId == null) {
|
||||
return Future.value([]);
|
||||
}
|
||||
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where(
|
||||
(row) =>
|
||||
row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not(),
|
||||
)
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<ExifInfo?> getExif(String id) {
|
||||
return _db.managers.remoteExifEntity
|
||||
.filter((row) => row.assetId.id.equals(id))
|
||||
@@ -146,4 +189,53 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> stack(String userId, StackResponse stack) {
|
||||
return _db.transaction(() async {
|
||||
final stackIds = await _db.managers.stackEntity
|
||||
.filter((row) => row.primaryAssetId.isIn(stack.assetIds))
|
||||
.map((row) => row.id)
|
||||
.get();
|
||||
|
||||
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||
|
||||
await _db.batch((batch) {
|
||||
final companion = StackEntityCompanion(
|
||||
ownerId: Value(userId),
|
||||
primaryAssetId: Value(stack.primaryAssetId),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.stackEntity,
|
||||
companion.copyWith(id: Value(stack.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
|
||||
for (final assetId in stack.assetIds) {
|
||||
batch.update(
|
||||
_db.remoteAssetEntity,
|
||||
RemoteAssetEntityCompanion(
|
||||
stackId: Value(stack.id),
|
||||
),
|
||||
where: (e) => e.id.equals(assetId),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> unStack(List<String> stackIds) {
|
||||
return _db.transaction(() async {
|
||||
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||
|
||||
// TODO: delete this after adding foreign key on stackId
|
||||
await _db.batch((batch) {
|
||||
batch.update(
|
||||
_db.remoteAssetEntity,
|
||||
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
||||
where: (e) => e.stackId.isIn(stackIds),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'
|
||||
hide AssetVisibility;
|
||||
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class SearchApiRepository extends ApiRepository {
|
||||
final SearchApi _api;
|
||||
const SearchApiRepository(this._api);
|
||||
|
||||
Future<SearchResponseDto?> search(SearchFilter filter, int page) {
|
||||
AssetTypeEnum? type;
|
||||
if (filter.mediaType.index == AssetType.image.index) {
|
||||
type = AssetTypeEnum.IMAGE;
|
||||
} else if (filter.mediaType.index == AssetType.video.index) {
|
||||
type = AssetTypeEnum.VIDEO;
|
||||
}
|
||||
|
||||
if (filter.context != null && filter.context!.isNotEmpty) {
|
||||
return _api.searchSmart(
|
||||
SmartSearchDto(
|
||||
query: filter.context!,
|
||||
language: filter.language,
|
||||
country: filter.location.country,
|
||||
state: filter.location.state,
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
visibility: filter.display.isArchive
|
||||
? AssetVisibility.archive
|
||||
: AssetVisibility.timeline,
|
||||
isFavorite: filter.display.isFavorite ? true : null,
|
||||
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 1000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _api.searchAssets(
|
||||
MetadataSearchDto(
|
||||
originalFileName: filter.filename != null && filter.filename!.isNotEmpty
|
||||
? filter.filename
|
||||
: null,
|
||||
country: filter.location.country,
|
||||
description:
|
||||
filter.description != null && filter.description!.isNotEmpty
|
||||
? filter.description
|
||||
: null,
|
||||
state: filter.location.state,
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
model: filter.camera.model,
|
||||
takenAfter: filter.date.takenAfter,
|
||||
takenBefore: filter.date.takenBefore,
|
||||
visibility: filter.display.isArchive
|
||||
? AssetVisibility.archive
|
||||
: AssetVisibility.timeline,
|
||||
isFavorite: filter.display.isFavorite ? true : null,
|
||||
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||
personIds: filter.people.map((e) => e.id).toList(),
|
||||
type: type,
|
||||
page: page,
|
||||
size: 1000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>?> getSearchSuggestions(
|
||||
SearchSuggestionType type, {
|
||||
String? country,
|
||||
String? state,
|
||||
String? make,
|
||||
String? model,
|
||||
}) =>
|
||||
_api.getSearchSuggestions(
|
||||
type,
|
||||
country: country,
|
||||
state: state,
|
||||
make: make,
|
||||
model: model,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
@@ -7,8 +8,9 @@ class StorageRepository {
|
||||
const StorageRepository();
|
||||
|
||||
Future<File?> getFileForAsset(String assetId) async {
|
||||
final log = Logger('StorageRepository');
|
||||
File? file;
|
||||
final log = Logger('StorageRepository');
|
||||
|
||||
try {
|
||||
final entity = await AssetEntity.fromId(assetId);
|
||||
file = await entity?.originFile;
|
||||
@@ -20,4 +22,48 @@ class StorageRepository {
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
||||
File? file;
|
||||
final log = Logger('StorageRepository');
|
||||
|
||||
try {
|
||||
final entity = await AssetEntity.fromId(asset.id);
|
||||
file = await entity?.originFileWithSubtype;
|
||||
if (file == null) {
|
||||
log.warning(
|
||||
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||
);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
log.warning(
|
||||
"Error getting motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset) async {
|
||||
final log = Logger('StorageRepository');
|
||||
|
||||
AssetEntity? entity;
|
||||
|
||||
try {
|
||||
entity = await AssetEntity.fromId(asset.id);
|
||||
if (entity == null) {
|
||||
log.warning(
|
||||
"Cannot get AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||
);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
log.warning(
|
||||
"Error getting AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class SyncApiRepository {
|
||||
SyncRequestType.stacksV1,
|
||||
SyncRequestType.partnerStacksV1,
|
||||
SyncRequestType.userMetadataV1,
|
||||
SyncRequestType.peopleV1,
|
||||
],
|
||||
).toJson(),
|
||||
);
|
||||
@@ -173,6 +174,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
|
||||
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
|
||||
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
||||
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||
};
|
||||
|
||||
class _SyncAckV1 {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
@@ -137,6 +138,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
deletedAt: Value(asset.deletedAt),
|
||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||
stackId: Value(asset.stackId),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
@@ -510,6 +512,47 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updatePeopleV1(Iterable<SyncPersonV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final person in data) {
|
||||
final companion = PersonEntityCompanion(
|
||||
createdAt: Value(person.createdAt),
|
||||
updatedAt: Value(person.updatedAt),
|
||||
ownerId: Value(person.ownerId),
|
||||
name: Value(person.name),
|
||||
faceAssetId: Value(person.faceAssetId),
|
||||
isFavorite: Value(person.isFavorite),
|
||||
isHidden: Value(person.isHidden),
|
||||
color: Value(person.color),
|
||||
birthDate: Value(person.birthDate),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.personEntity,
|
||||
companion.copyWith(id: Value(person.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updatePeopleV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deletePeopleV1(
|
||||
Iterable<SyncPersonDeleteV1> data,
|
||||
) async {
|
||||
try {
|
||||
await _db.personEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((e) => e.personId)),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deletePeopleV1', error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetTypeEnum {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
@@ -88,6 +89,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
isFavorite: row.isFavorite,
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
livePhotoVideoId: row.livePhotoVideoId,
|
||||
stackId: row.stackId,
|
||||
stackCount: row.stackCount,
|
||||
)
|
||||
: LocalAsset(
|
||||
id: row.localId!,
|
||||
@@ -164,15 +167,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.localAssetEntity.checksum
|
||||
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
],
|
||||
)
|
||||
..addColumns([_db.remoteAssetEntity.id])
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||
..limit(count, offset: offset);
|
||||
|
||||
return query
|
||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||
.get();
|
||||
return query.map((row) {
|
||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||
return asset.copyWith(
|
||||
remoteId: row.read(_db.remoteAssetEntity.id),
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||
@@ -195,41 +208,75 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
return _db.remoteAlbumAssetEntity
|
||||
.count(where: (row) => row.albumId.equals(albumId))
|
||||
.map(_generateBuckets)
|
||||
.watchSingle();
|
||||
.watch()
|
||||
.map((results) => results.isNotEmpty ? results.first : <Bucket>[])
|
||||
.handleError((error) {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||
return (_db.remoteAlbumEntity.select()
|
||||
..where((row) => row.id.equals(albumId)))
|
||||
.watch()
|
||||
.switchMap((albums) {
|
||||
if (albums.isEmpty) {
|
||||
return Stream.value(<Bucket>[]);
|
||||
}
|
||||
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.remoteAlbumAssetEntity,
|
||||
_db.remoteAlbumAssetEntity.assetId
|
||||
.equalsExp(_db.remoteAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
|
||||
)
|
||||
..groupBy([dateExp])
|
||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||
final album = albums.first;
|
||||
final isAscending = album.order == AlbumAssetOrder.asc;
|
||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||
|
||||
return query.map((row) {
|
||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||
final assetCount = row.read(assetCountExp)!;
|
||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||
}).watch();
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.remoteAlbumAssetEntity,
|
||||
_db.remoteAlbumAssetEntity.assetId
|
||||
.equalsExp(_db.remoteAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
|
||||
)
|
||||
..groupBy([dateExp]);
|
||||
|
||||
if (isAscending) {
|
||||
query.orderBy([OrderingTerm.asc(dateExp)]);
|
||||
} else {
|
||||
query.orderBy([OrderingTerm.desc(dateExp)]);
|
||||
}
|
||||
|
||||
return query.map((row) {
|
||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||
final assetCount = row.read(assetCountExp)!;
|
||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||
}).watch();
|
||||
}).handleError((error) {
|
||||
// If there's an error (e.g., album was deleted), return empty buckets
|
||||
return <Bucket>[];
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(
|
||||
String albumId, {
|
||||
required int offset,
|
||||
required int count,
|
||||
}) {
|
||||
}) async {
|
||||
final albumData = await (_db.remoteAlbumEntity.select()
|
||||
..where((row) => row.id.equals(albumId)))
|
||||
.getSingleOrNull();
|
||||
|
||||
// If album doesn't exist (was deleted), return empty list
|
||||
if (albumData == null) {
|
||||
return <BaseAsset>[];
|
||||
}
|
||||
|
||||
final isAscending = albumData.order == AlbumAssetOrder.asc;
|
||||
|
||||
final query = _db.remoteAssetEntity.select().join(
|
||||
[
|
||||
innerJoin(
|
||||
@@ -239,18 +286,30 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
useColumns: false,
|
||||
),
|
||||
],
|
||||
)
|
||||
..where(
|
||||
)..where(
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
|
||||
)
|
||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||
..limit(count, offset: offset);
|
||||
);
|
||||
|
||||
if (isAscending) {
|
||||
query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
|
||||
} else {
|
||||
query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]);
|
||||
}
|
||||
|
||||
query.limit(count, offset: offset);
|
||||
|
||||
return query
|
||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||
.get();
|
||||
}
|
||||
|
||||
TimelineQuery fromAssets(List<BaseAsset> assets) => (
|
||||
bucketSource: () => Stream.value(_generateBuckets(assets.length)),
|
||||
assetSource: (offset, count) =>
|
||||
Future.value(assets.skip(offset).take(count).toList()),
|
||||
);
|
||||
|
||||
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
|
||||
_remoteQueryBuilder(
|
||||
filter: (row) =>
|
||||
|
||||
+34
-1
@@ -31,6 +31,7 @@ import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/utils/download.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/utils/licenses.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -92,12 +93,30 @@ Future<void> initApp() async {
|
||||
|
||||
initializeTimeZones();
|
||||
|
||||
// Initialize the file downloader
|
||||
|
||||
await FileDownloader().configure(
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
globalConfig: (Config.holdingQueue, (6, 6, 3)),
|
||||
);
|
||||
|
||||
await FileDownloader().trackTasksInGroup(
|
||||
downloadGroupLivePhoto,
|
||||
markDownloadedComplete: false,
|
||||
);
|
||||
|
||||
await FileDownloader().trackTasks();
|
||||
|
||||
LicenseRegistry.addLicense(
|
||||
() async* {
|
||||
for (final license in nonPubLicenses.entries) {
|
||||
yield LicenseEntryWithLineBreaks(
|
||||
[license.key],
|
||||
license.value,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class ImmichApp extends ConsumerStatefulWidget {
|
||||
@@ -159,7 +178,21 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
}
|
||||
|
||||
void _configureFileDownloaderNotifications() {
|
||||
FileDownloader().configureNotification(
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
downloadGroupImage,
|
||||
running: TaskNotification(
|
||||
'downloading_media'.tr(),
|
||||
'${'file_name'.tr()}: {filename}',
|
||||
),
|
||||
complete: TaskNotification(
|
||||
'download_finished'.tr(),
|
||||
'${'file_name'.tr()}: {filename}',
|
||||
),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
downloadGroupVideo,
|
||||
running: TaskNotification(
|
||||
'downloading_media'.tr(),
|
||||
'${'file_name'.tr()}: {filename}',
|
||||
|
||||
@@ -237,7 +237,7 @@ class SearchFilter {
|
||||
String? filename;
|
||||
String? description;
|
||||
String? language;
|
||||
Set<Person> people;
|
||||
Set<PersonDto> people;
|
||||
SearchLocationFilter location;
|
||||
SearchCameraFilter camera;
|
||||
SearchDateFilter date;
|
||||
@@ -282,7 +282,7 @@ class SearchFilter {
|
||||
String? filename,
|
||||
String? description,
|
||||
String? language,
|
||||
Set<Person>? people,
|
||||
Set<PersonDto>? people,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
SearchDateFilter? date,
|
||||
|
||||
@@ -17,7 +17,7 @@ enum UploadStatus {
|
||||
notFound,
|
||||
failed,
|
||||
canceled,
|
||||
waitingtoRetry,
|
||||
waitingToRetry,
|
||||
paused,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,40 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class AlbumControlButton extends ConsumerWidget {
|
||||
void Function() onAddPhotosPressed;
|
||||
void Function() onAddUsersPressed;
|
||||
final void Function()? onAddPhotosPressed;
|
||||
final void Function()? onAddUsersPressed;
|
||||
|
||||
AlbumControlButton({
|
||||
const AlbumControlButton({
|
||||
super.key,
|
||||
required this.onAddPhotosPressed,
|
||||
required this.onAddUsersPressed,
|
||||
this.onAddPhotosPressed,
|
||||
this.onAddUsersPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userId = ref.watch(authProvider).userId;
|
||||
final isOwner = ref.watch(
|
||||
currentAlbumProvider.select((album) {
|
||||
return album?.ownerId == userId;
|
||||
}),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
if (onAddPhotosPressed != null)
|
||||
AlbumActionFilledButton(
|
||||
key: const ValueKey('add_photos_button'),
|
||||
iconData: Icons.add_photo_alternate_outlined,
|
||||
onPressed: onAddPhotosPressed,
|
||||
labelText: "add_photos".tr(),
|
||||
),
|
||||
if (isOwner)
|
||||
AlbumActionFilledButton(
|
||||
key: const ValueKey('add_users_button'),
|
||||
iconData: Icons.person_add_alt_rounded,
|
||||
onPressed: onAddUsersPressed,
|
||||
labelText: "album_viewer_page_share_add_users".tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (onAddUsersPressed != null)
|
||||
AlbumActionFilledButton(
|
||||
key: const ValueKey('add_users_button'),
|
||||
iconData: Icons.person_add_alt_rounded,
|
||||
onPressed: onAddUsersPressed,
|
||||
labelText: "album_viewer_page_share_add_users".tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ class AlbumViewer extends HookConsumerWidget {
|
||||
final userId = ref.watch(authProvider).userId;
|
||||
final isMultiselecting = ref.watch(multiselectProvider);
|
||||
final isProcessing = useProcessingOverlay();
|
||||
final isOwner = ref.watch(
|
||||
currentAlbumProvider.select((album) {
|
||||
return album?.ownerId == userId;
|
||||
}),
|
||||
);
|
||||
|
||||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||
final bool isSuccess =
|
||||
@@ -138,10 +143,13 @@ class AlbumViewer extends HookConsumerWidget {
|
||||
),
|
||||
const AlbumSharedUserIcons(),
|
||||
if (album.isRemote)
|
||||
AlbumControlButton(
|
||||
key: const ValueKey("albumControlButton"),
|
||||
onAddPhotosPressed: onAddPhotosPressed,
|
||||
onAddUsersPressed: onAddUsersPressed,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: AlbumControlButton(
|
||||
key: const ValueKey("albumControlButton"),
|
||||
onAddPhotosPressed: onAddPhotosPressed,
|
||||
onAddUsersPressed: isOwner ? onAddUsersPressed : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
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';
|
||||
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/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftBackupPage extends ConsumerStatefulWidget {
|
||||
const DriftBackupPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DriftBackupPage> createState() => _DriftBackupPageState();
|
||||
}
|
||||
|
||||
class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||
}
|
||||
|
||||
Future<void> startBackup() async {
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||
await ref.read(driftBackupProvider.notifier).backup();
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
await ref.read(driftBackupProvider.notifier).cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedAlbum = ref
|
||||
.watch(backupAlbumProvider)
|
||||
.where(
|
||||
(album) => album.backupSelection == BackupSelection.selected,
|
||||
)
|
||||
.toList();
|
||||
final uploadItems = ref.watch(
|
||||
driftBackupProvider.select((state) => state.uploadItems),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
"backup_controller_page_backup".t(),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.maybePop(true);
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_rounded,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 16,
|
||||
bottom: 32,
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const _BackupAlbumSelectionCard(),
|
||||
if (selectedAlbum.isNotEmpty) ...[
|
||||
const _TotalCard(),
|
||||
const _BackupCard(),
|
||||
const _RemainderCard(),
|
||||
const Divider(),
|
||||
BackupToggleButton(
|
||||
onStart: () async => await startBackup(),
|
||||
onStop: () async => await stopBackup(),
|
||||
),
|
||||
if (uploadItems.isNotEmpty)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.info_outline_rounded),
|
||||
onPressed: () => context.pushRoute(
|
||||
const DriftUploadDetailRoute(),
|
||||
),
|
||||
label: Text("view_details".t(context: context)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupAlbumSelectionCard extends ConsumerWidget {
|
||||
const _BackupAlbumSelectionCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget buildSelectedAlbumName() {
|
||||
String text = "backup_controller_page_backup_selected".tr();
|
||||
final albums = ref
|
||||
.watch(backupAlbumProvider)
|
||||
.where(
|
||||
(album) => album.backupSelection == BackupSelection.selected,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (albums.isNotEmpty) {
|
||||
for (var album in albums) {
|
||||
if (album.name == "Recent" || album.name == "Recents") {
|
||||
text += "${album.name} (${'all'.tr()}), ";
|
||||
} else {
|
||||
text += "${album.name}, ";
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
text.trim().substring(0, text.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
"backup_controller_page_none_selected".tr(),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildExcludedAlbumName() {
|
||||
String text = "backup_controller_page_excluded".tr();
|
||||
final albums = ref
|
||||
.watch(backupAlbumProvider)
|
||||
.where(
|
||||
(album) => album.backupSelection == BackupSelection.excluded,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (albums.isNotEmpty) {
|
||||
for (var album in albums) {
|
||||
text += "${album.name}, ";
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
text.trim().substring(0, text.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
elevation: 0,
|
||||
borderOnForeground: false,
|
||||
child: ListTile(
|
||||
minVerticalPadding: 18,
|
||||
title: Text(
|
||||
"backup_controller_page_albums",
|
||||
style: context.textTheme.titleMedium,
|
||||
).tr(),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"backup_controller_page_to_backup",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
).tr(),
|
||||
buildSelectedAlbumName(),
|
||||
buildExcludedAlbumName(),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.pushRoute(const DriftBackupAlbumSelectionRoute());
|
||||
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||
},
|
||||
child: const Text(
|
||||
"select",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TotalCard extends ConsumerWidget {
|
||||
const _TotalCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final totalCount =
|
||||
ref.watch(driftBackupProvider.select((p) => p.totalCount));
|
||||
|
||||
return BackupInfoCard(
|
||||
title: "total".tr(),
|
||||
subtitle: "backup_controller_page_total_sub".tr(),
|
||||
info: totalCount.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupCard extends ConsumerWidget {
|
||||
const _BackupCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final backupCount =
|
||||
ref.watch(driftBackupProvider.select((p) => p.backupCount));
|
||||
|
||||
return BackupInfoCard(
|
||||
title: "backup_controller_page_backup".tr(),
|
||||
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||
info: backupCount.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RemainderCard extends ConsumerWidget {
|
||||
const _RemainderCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final remainderCount =
|
||||
ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
||||
return BackupInfoCard(
|
||||
title: "backup_controller_page_remainder".tr(),
|
||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||
info: remainderCount.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/album/local_album.model.dart';
|
||||
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||
const DriftBackupAlbumSelectionPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DriftBackupAlbumSelectionPage> createState() =>
|
||||
_DriftBackupAlbumSelectionPageState();
|
||||
}
|
||||
|
||||
class _DriftBackupAlbumSelectionPageState
|
||||
extends ConsumerState<DriftBackupAlbumSelectionPage> {
|
||||
String _searchQuery = '';
|
||||
bool _isSearchMode = false;
|
||||
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||
late TextEditingController _searchController;
|
||||
late FocusNode _searchFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_enableSyncUploadAlbum = ValueNotifier<bool>(false);
|
||||
_searchController = TextEditingController();
|
||||
_searchFocusNode = FocusNode();
|
||||
|
||||
_enableSyncUploadAlbum.value = ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.syncAlbums);
|
||||
ref.read(backupAlbumProvider.notifier).getAll();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_enableSyncUploadAlbum.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albums = ref.watch(backupAlbumProvider);
|
||||
final albumCount = albums.length;
|
||||
// Filter albums based on search query
|
||||
final filteredAlbums = albums.where((album) {
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
return album.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
}).toList();
|
||||
|
||||
final selectedBackupAlbums = albums
|
||||
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||
.toList();
|
||||
final excludedBackupAlbums = albums
|
||||
.where((album) => album.backupSelection == BackupSelection.excluded)
|
||||
.toList();
|
||||
|
||||
handleSyncAlbumToggle(bool isEnable) async {
|
||||
if (isEnable) {
|
||||
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
for (final album in selectedBackupAlbums) {
|
||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: _isSearchMode
|
||||
? SearchField(
|
||||
hintText: 'search_albums'.t(context: context),
|
||||
autofocus: true,
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
onChanged: (value) =>
|
||||
setState(() => _searchQuery = value.trim()),
|
||||
)
|
||||
: const Text(
|
||||
"backup_album_selection_page_select_albums",
|
||||
).t(context: context),
|
||||
actions: [
|
||||
if (!_isSearchMode)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => setState(() {
|
||||
_isSearchMode = true;
|
||||
_searchQuery = '';
|
||||
}),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => setState(() {
|
||||
_isSearchMode = false;
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
}),
|
||||
),
|
||||
],
|
||||
elevation: 0,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Text(
|
||||
"backup_album_selection_page_selection_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).t(context: context),
|
||||
),
|
||||
// Selected Album Chips
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
_SelectedAlbumNameChips(
|
||||
selectedBackupAlbums: selectedBackupAlbums,
|
||||
),
|
||||
_ExcludedAlbumNameChips(
|
||||
excludedBackupAlbums: excludedBackupAlbums,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: _enableSyncUploadAlbum,
|
||||
title: "sync_albums".t(context: context),
|
||||
subtitle:
|
||||
"sync_upload_album_setting_subtitle".t(context: context),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
onChanged: handleSyncAlbumToggle,
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: Text(
|
||||
"albums_on_device_count".t(
|
||||
context: context,
|
||||
args: {'count': albumCount.toString()},
|
||||
),
|
||||
style: context.textTheme.titleSmall,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_albums_tap",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).t(context: context),
|
||||
),
|
||||
trailing: IconButton(
|
||||
splashRadius: 16,
|
||||
icon: Icon(
|
||||
Icons.info,
|
||||
size: 20,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
onPressed: () {
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
'backup_album_selection_page_selection_info',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).t(context: context),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text(
|
||||
'backup_album_selection_page_assets_scatter',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).t(context: context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
if (Platform.isAndroid)
|
||||
_SelectAllButton(
|
||||
filteredAlbums: filteredAlbums,
|
||||
selectedBackupAlbums: selectedBackupAlbums,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.crossAxisExtent > 600) {
|
||||
return _AlbumSelectionGrid(
|
||||
filteredAlbums: filteredAlbums,
|
||||
searchQuery: _searchQuery,
|
||||
);
|
||||
} else {
|
||||
return _AlbumSelectionList(
|
||||
filteredAlbums: filteredAlbums,
|
||||
searchQuery: _searchQuery,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumSelectionList extends StatelessWidget {
|
||||
final List<LocalAlbum> filteredAlbums;
|
||||
final String searchQuery;
|
||||
|
||||
const _AlbumSelectionList({
|
||||
required this.filteredAlbums,
|
||||
required this.searchQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Text('album_search_not_found'.t(context: context)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredAlbums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
((context, index) {
|
||||
return DriftAlbumInfoListTile(
|
||||
album: filteredAlbums[index],
|
||||
);
|
||||
}),
|
||||
childCount: filteredAlbums.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumSelectionGrid extends StatelessWidget {
|
||||
final List<LocalAlbum> filteredAlbums;
|
||||
final String searchQuery;
|
||||
|
||||
const _AlbumSelectionGrid({
|
||||
required this.filteredAlbums,
|
||||
required this.searchQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Text('album_search_not_found'.t(context: context)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredAlbums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 300,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
itemCount: filteredAlbums.length,
|
||||
itemBuilder: ((context, index) {
|
||||
return DriftAlbumInfoListTile(
|
||||
album: filteredAlbums[index],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectedAlbumNameChips extends ConsumerWidget {
|
||||
final List<LocalAlbum> selectedBackupAlbums;
|
||||
|
||||
const _SelectedAlbumNameChips({
|
||||
required this.selectedBackupAlbums,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Wrap(
|
||||
children: selectedBackupAlbums.asMap().entries.map((entry) {
|
||||
final album = entry.value;
|
||||
|
||||
void removeSelection() {
|
||||
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: GestureDetector(
|
||||
onTap: removeSelection,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Chip(
|
||||
label: Text(
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: context.primaryColor,
|
||||
deleteIconColor:
|
||||
context.isDarkTheme ? Colors.black : Colors.white,
|
||||
deleteIcon: const Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 15,
|
||||
),
|
||||
onDeleted: removeSelection,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExcludedAlbumNameChips extends ConsumerWidget {
|
||||
final List<LocalAlbum> excludedBackupAlbums;
|
||||
|
||||
const _ExcludedAlbumNameChips({
|
||||
required this.excludedBackupAlbums,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Wrap(
|
||||
children: excludedBackupAlbums.asMap().entries.map((entry) {
|
||||
final album = entry.value;
|
||||
|
||||
void removeSelection() {
|
||||
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: removeSelection,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Chip(
|
||||
label: Text(
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: context.scaffoldBackgroundColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.red[300],
|
||||
deleteIconColor: context.scaffoldBackgroundColor,
|
||||
deleteIcon: const Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 15,
|
||||
),
|
||||
onDeleted: removeSelection,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectAllButton extends ConsumerWidget {
|
||||
final List<LocalAlbum> filteredAlbums;
|
||||
final List<LocalAlbum> selectedBackupAlbums;
|
||||
|
||||
const _SelectAllButton({
|
||||
required this.filteredAlbums,
|
||||
required this.selectedBackupAlbums,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final canSelectAll = filteredAlbums
|
||||
.where((album) => album.backupSelection != BackupSelection.selected)
|
||||
.isNotEmpty;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: canSelectAll
|
||||
? () {
|
||||
for (final album in filteredAlbums) {
|
||||
if (album.backupSelection != BackupSelection.selected) {
|
||||
ref
|
||||
.read(backupAlbumProvider.notifier)
|
||||
.selectAlbum(album);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.select_all),
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
"select_all".t(context: context),
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: selectedBackupAlbums.isNotEmpty
|
||||
? () {
|
||||
for (final album in filteredAlbums) {
|
||||
if (album.backupSelection == BackupSelection.selected) {
|
||||
ref
|
||||
.read(backupAlbumProvider.notifier)
|
||||
.deselectAlbum(album);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.deselect),
|
||||
label: Text('deselect_all'.t(context: context)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
@RoutePage()
|
||||
class DriftUploadDetailPage extends ConsumerWidget {
|
||||
const DriftUploadDetailPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final uploadItems = ref.watch(
|
||||
driftBackupProvider.select((state) => state.uploadItems),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("upload_details".t(context: context)),
|
||||
backgroundColor: context.colorScheme.surface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
),
|
||||
body: uploadItems.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: _buildUploadList(uploadItems),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_upload_outlined,
|
||||
size: 80,
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"no_uploads_in_progress".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadList(
|
||||
Map<String, DriftUploadStatus> uploadItems,
|
||||
) {
|
||||
return ListView.separated(
|
||||
addAutomaticKeepAlives: true,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: uploadItems.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final item = uploadItems.values.elementAt(index);
|
||||
return _buildUploadCard(context, item);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadCard(
|
||||
BuildContext context,
|
||||
DriftUploadStatus item,
|
||||
) {
|
||||
final isCompleted = item.progress >= 1.0;
|
||||
final double progressPercentage = (item.progress * 100).clamp(0, 100);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _showFileDetailDialog(context, item),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
path.basename(item.filename),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tap for more details',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildProgressIndicator(
|
||||
context,
|
||||
item.progress,
|
||||
progressPercentage,
|
||||
isCompleted,
|
||||
item.networkSpeedAsString,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator(
|
||||
BuildContext context,
|
||||
double progress,
|
||||
double percentage,
|
||||
bool isCompleted,
|
||||
String networkSpeedAsString,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween<double>(begin: 0.0, end: progress),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
backgroundColor:
|
||||
context.colorScheme.outline.withValues(alpha: 0.2),
|
||||
strokeWidth: 3,
|
||||
value: value,
|
||||
color: isCompleted
|
||||
? context.colorScheme.primary
|
||||
: context.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 28,
|
||||
color: context.colorScheme.primary,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
percentage.toStringAsFixed(0),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
networkSpeedAsString,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showFileDetailDialog(
|
||||
BuildContext context,
|
||||
DriftUploadStatus item,
|
||||
) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FileDetailDialog(uploadStatus: item),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FileDetailDialog extends ConsumerWidget {
|
||||
final DriftUploadStatus uploadStatus;
|
||||
|
||||
const FileDetailDialog({
|
||||
super.key,
|
||||
required this.uploadStatus,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AlertDialog(
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: context.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"details".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: FutureBuilder<LocalAsset?>(
|
||||
future: _getAssetDetails(ref, uploadStatus.taskId),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 200,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final asset = snapshot.data;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Thumbnail at the top center
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Container(
|
||||
width: 128,
|
||||
height: 128,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline
|
||||
.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: asset != null
|
||||
? Thumbnail(
|
||||
asset: asset,
|
||||
size: const Size(512, 512),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (asset != null) ...[
|
||||
_buildInfoSection(context, [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Filename",
|
||||
path.basename(uploadStatus.filename),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Local ID",
|
||||
asset.id,
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"File Size",
|
||||
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||
),
|
||||
if (asset.width != null)
|
||||
_buildInfoRow(context, "Width", "${asset.width}px"),
|
||||
if (asset.height != null)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Height",
|
||||
"${asset.height}px",
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Created At",
|
||||
asset.createdAt.toString(),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Updated At",
|
||||
asset.updatedAt.toString(),
|
||||
),
|
||||
if (asset.checksum != null)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Checksum",
|
||||
asset.checksum!,
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
"close".t(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoSection(
|
||||
BuildContext context,
|
||||
List<Widget> children,
|
||||
) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...children,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
"$label:",
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.labelMedium?.copyWith(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<LocalAsset?> _getAssetDetails(
|
||||
WidgetRef ref,
|
||||
String localAssetId,
|
||||
) async {
|
||||
try {
|
||||
final repository = ref.read(localAssetRepository);
|
||||
return await repository.getById(localAssetId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.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/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ChangeExperiencePage extends ConsumerStatefulWidget {
|
||||
final bool switchingToBeta;
|
||||
|
||||
const ChangeExperiencePage({super.key, required this.switchingToBeta});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _ChangeExperiencePageState();
|
||||
}
|
||||
|
||||
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
bool hasMigrated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration());
|
||||
}
|
||||
|
||||
Future<void> _handleMigration() 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();
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
HapticFeedback.heavyImpact();
|
||||
hasMigrated = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Durations.long4,
|
||||
child: hasMigrated
|
||||
? const Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
size: 48.0,
|
||||
)
|
||||
: const SizedBox(
|
||||
width: 50.0,
|
||||
height: 50.0,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 300.0,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.long4,
|
||||
child: hasMigrated
|
||||
? Text(
|
||||
"Migration success!",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: Text(
|
||||
"Data migration in progress...\nPlease wait and don't close this page",
|
||||
style: context.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasMigrated)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.replaceRoute(
|
||||
widget.switchingToBeta
|
||||
? const TabShellRoute()
|
||||
: const TabControllerRoute(),
|
||||
);
|
||||
},
|
||||
child: const Text("Continue"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final asset = loadAsset(currentIndex.value);
|
||||
|
||||
if (asset.isRemote) {
|
||||
ref.read(castProvider.notifier).loadMedia(asset, false);
|
||||
ref.read(castProvider.notifier).loadMediaOld(asset, false);
|
||||
} else {
|
||||
if (isCasting) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -394,7 +394,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
||||
// send image to casting if the server has it
|
||||
if (newAsset.isRemote) {
|
||||
ref.read(castProvider.notifier).loadMedia(newAsset, false);
|
||||
ref.read(castProvider.notifier).loadMediaOld(newAsset, false);
|
||||
} else {
|
||||
context.scaffoldMessenger.clearSnackBars();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||
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/beta_timeline_list_tile.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';
|
||||
@@ -94,55 +95,59 @@ class _MobileLayout extends StatelessWidget {
|
||||
const _MobileLayout();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
children: SettingSection.values
|
||||
.map(
|
||||
(setting) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
final List<Widget> settings = SettingSection.values
|
||||
.map(
|
||||
(setting) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
color: context.isDarkTheme
|
||||
? Colors.black26
|
||||
: Colors.white.withAlpha(100),
|
||||
),
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
color: context.isDarkTheme
|
||||
? Colors.black26
|
||||
: Colors.white.withAlpha(100),
|
||||
),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(setting.icon, color: context.primaryColor),
|
||||
),
|
||||
title: Text(
|
||||
setting.title,
|
||||
style: context.textTheme.titleMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
subtitle: Text(
|
||||
setting.subtitle,
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
onTap: () =>
|
||||
context.pushRoute(SettingsSubRoute(section: setting)),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(setting.icon, color: context.primaryColor),
|
||||
),
|
||||
title: Text(
|
||||
setting.title,
|
||||
style: context.textTheme.titleMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
subtitle: Text(
|
||||
setting.subtitle,
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
onTap: () =>
|
||||
context.pushRoute(SettingsSubRoute(section: setting)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
|
||||
children: [
|
||||
const BetaTimelineListTile(),
|
||||
...settings,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,15 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
}
|
||||
|
||||
if (context.router.current.name == SplashScreenRoute.name) {
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
context.replaceRoute(
|
||||
Store.isBetaTimelineEnabled
|
||||
? const TabShellRoute()
|
||||
: const TabControllerRoute(),
|
||||
);
|
||||
}
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final hasPermission =
|
||||
|
||||
@@ -3,45 +3,50 @@ 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';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.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/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/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 ConsumerWidget {
|
||||
class TabShellPage extends ConsumerStatefulWidget {
|
||||
const TabShellPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
ConsumerState<TabShellPage> createState() => _TabShellPageState();
|
||||
}
|
||||
|
||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||
if (!isProcessing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -18,
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
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) {
|
||||
await ref.read(driftBackupProvider.notifier).handleBackupResume();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
|
||||
final navigationDestinations = [
|
||||
NavigationDestination(
|
||||
@@ -49,12 +54,9 @@ class TabShellPage extends ConsumerWidget {
|
||||
icon: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
@@ -72,12 +74,9 @@ class TabShellPage extends ConsumerWidget {
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
@@ -85,12 +84,9 @@ class TabShellPage extends ConsumerWidget {
|
||||
icon: const Icon(
|
||||
Icons.space_dashboard_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -117,7 +113,7 @@ class TabShellPage extends ConsumerWidget {
|
||||
return AutoTabsRouter(
|
||||
routes: [
|
||||
const MainTimelineRoute(),
|
||||
SearchRoute(),
|
||||
DriftSearchRoute(),
|
||||
const DriftAlbumsRoute(),
|
||||
const DriftLibraryRoute(),
|
||||
],
|
||||
@@ -167,7 +163,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
|
||||
// Album page
|
||||
if (index == 2) {
|
||||
ref.read(remoteAlbumProvider.notifier).getAll();
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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:flutter_hooks/flutter_hooks.dart' show useState;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/local_auth.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/forms/pin_registration_form.dart';
|
||||
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PinAuthPage extends HookConsumerWidget {
|
||||
@@ -19,6 +20,7 @@ class PinAuthPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final localAuthState = ref.watch(localAuthProvider);
|
||||
final showPinRegistrationForm = useState(createPinCode);
|
||||
final isBetaTimeline = Store.isBetaTimelineEnabled;
|
||||
|
||||
Future<void> registerBiometric(String pinCode) async {
|
||||
final isRegistered =
|
||||
@@ -39,7 +41,11 @@ class PinAuthPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
context.replaceRoute(const LockedRoute());
|
||||
if (isBetaTimeline) {
|
||||
context.replaceRoute(const DriftLockedFolderRoute());
|
||||
} else {
|
||||
context.replaceRoute(const LockedRoute());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +99,14 @@ class PinAuthPage extends HookConsumerWidget {
|
||||
Center(
|
||||
child: PinVerificationForm(
|
||||
autoFocus: true,
|
||||
onSuccess: (_) =>
|
||||
context.replaceRoute(const LockedRoute()),
|
||||
onSuccess: (_) {
|
||||
if (isBetaTimeline) {
|
||||
context
|
||||
.replaceRoute(const DriftLockedFolderRoute());
|
||||
} else {
|
||||
context.replaceRoute(const LockedRoute());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftPartnerPage extends HookConsumerWidget {
|
||||
const DriftPartnerPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final potentialPartnersAsync = ref.watch(driftAvailablePartnerProvider);
|
||||
|
||||
addNewUsersHandler() async {
|
||||
final potentialPartners = potentialPartnersAsync.value;
|
||||
if (potentialPartners == null || potentialPartners.isEmpty) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "partner_page_no_more_users".tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedUser = await showDialog<PartnerUserDto>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: const Text("partner_page_select_partner").tr(),
|
||||
children: [
|
||||
for (PartnerUserDto partner in potentialPartners)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => context.pop(partner),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: PartnerUserAvatar(partner: partner),
|
||||
),
|
||||
Text(partner.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (selectedUser != null) {
|
||||
await ref.read(partnerUsersProvider.notifier).addPartner(selectedUser);
|
||||
}
|
||||
}
|
||||
|
||||
onDeleteUser(PartnerUserDto partner) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ConfirmDialog(
|
||||
title: "stop_photo_sharing",
|
||||
content: "partner_page_stop_sharing_content"
|
||||
.tr(namedArgs: {'partner': partner.name}),
|
||||
onOk: () =>
|
||||
ref.read(partnerUsersProvider.notifier).removePartner(partner),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("partners").t(context: context),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: potentialPartnersAsync.whenOrNull(
|
||||
data: (data) => addNewUsersHandler,
|
||||
),
|
||||
icon: const Icon(Icons.person_add),
|
||||
tooltip: "add_partner".tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _SharedToPartnerList(
|
||||
onAddPartner: addNewUsersHandler,
|
||||
onDeletePartner: onDeleteUser,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SharedToPartnerList extends ConsumerWidget {
|
||||
final VoidCallback onAddPartner;
|
||||
final Function(PartnerUserDto partner) onDeletePartner;
|
||||
|
||||
const _SharedToPartnerList({
|
||||
required this.onAddPartner,
|
||||
required this.onDeletePartner,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final partnerAsync = ref.watch(driftSharedByPartnerProvider);
|
||||
|
||||
return partnerAsync.when(
|
||||
data: (partners) {
|
||||
if (partners.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: const Text(
|
||||
"partner_page_empty_message",
|
||||
style: TextStyle(fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onAddPartner,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text("add_partner").tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: partners.length,
|
||||
itemBuilder: (context, index) {
|
||||
final partner = partners[index];
|
||||
return ListTile(
|
||||
leading: PartnerUserAvatar(partner: partner),
|
||||
title: Text(partner.name),
|
||||
subtitle: Text(partner.email),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.person_remove),
|
||||
onPressed: () => onDeletePartner(partner),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Text("Error loading partners: $error"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
showPeoplePicker() {
|
||||
handleOnSelect(Set<Person> value) {
|
||||
handleOnSelect(Set<PersonDto> value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
people: value,
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
@@ -75,7 +76,9 @@ class ShareIntentPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.navigateTo(
|
||||
const TabControllerRoute(),
|
||||
Store.isBetaTimelineEnabled
|
||||
? const TabShellRoute()
|
||||
: const TabControllerRoute(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
@@ -265,7 +268,7 @@ class UploadStatusIcon extends StatelessWidget {
|
||||
color: Colors.red,
|
||||
semanticLabel: 'canceled'.tr(),
|
||||
),
|
||||
UploadStatus.waitingtoRetry || UploadStatus.paused => Icon(
|
||||
UploadStatus.waitingToRetry || UploadStatus.paused => Icon(
|
||||
Icons.pause_circle_rounded,
|
||||
color: context.primaryColor,
|
||||
semanticLabel: 'paused'.tr(),
|
||||
|
||||
@@ -22,16 +22,6 @@ final _features = [
|
||||
icon: Icons.timeline_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Video',
|
||||
icon: Icons.video_collection_outlined,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Recently Taken',
|
||||
icon: Icons.schedule_outlined,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Selection Mode Timeline',
|
||||
icon: Icons.developer_mode_rounded,
|
||||
@@ -101,6 +91,10 @@ final _features = [
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Local Data',
|
||||
style: const TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
@@ -111,6 +105,10 @@ final _features = [
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Remote Data',
|
||||
style: const TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
icon: Icons.delete_sweep_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
@@ -122,21 +120,34 @@ final _features = [
|
||||
await db.memoryEntity.deleteAll();
|
||||
await db.memoryAssetEntity.deleteAll();
|
||||
await db.stackEntity.deleteAll();
|
||||
await db.personEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
name: 'Local Media Summary',
|
||||
style: const TextStyle(
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
icon: Icons.table_chart_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Remote Media Summary',
|
||||
style: const TextStyle(
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
icon: Icons.summarize_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Reset Sqlite',
|
||||
icon: Icons.table_view_rounded,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
onTap: (_, ref) async {
|
||||
final drift = ref.read(driftProvider);
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -169,7 +180,10 @@ class FeatInDevPage extends StatelessWidget {
|
||||
final feat = _features[index];
|
||||
return Consumer(
|
||||
builder: (ctx, ref, _) => ListTile(
|
||||
title: Text(feat.name),
|
||||
title: Text(
|
||||
feat.name,
|
||||
style: feat.style,
|
||||
),
|
||||
trailing: Icon(feat.icon),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () => unawaited(feat.onTap(ctx, ref)),
|
||||
@@ -192,10 +206,12 @@ class _Feature {
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.style,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final TextStyle? style;
|
||||
final Future<void> Function(BuildContext, WidgetRef _) onTap;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class MainTimelinePage extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||
|
||||
return memoryLaneProvider.when(
|
||||
return memoryLaneProvider.maybeWhen(
|
||||
data: (memories) {
|
||||
return memories.isEmpty
|
||||
? const Timeline(showStorageIndicator: true)
|
||||
@@ -26,8 +26,7 @@ class MainTimelinePage extends ConsumerWidget {
|
||||
showStorageIndicator: true,
|
||||
);
|
||||
},
|
||||
loading: () => const Timeline(showStorageIndicator: true),
|
||||
error: (error, stackTrace) => const Timeline(showStorageIndicator: true),
|
||||
orElse: () => const Timeline(showStorageIndicator: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,10 @@ final _remoteStats = [
|
||||
name: 'Stacks',
|
||||
load: (db) => db.managers.stackEntity.count(),
|
||||
),
|
||||
_Stat(
|
||||
name: 'People',
|
||||
load: (db) => db.managers.personEntity.count(),
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||
@@ -39,7 +40,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
|
||||
// Load albums when component mounts
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(remoteAlbumProvider.notifier).getAll();
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
@@ -87,17 +88,20 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albumState = ref.watch(remoteAlbumProvider);
|
||||
final albums = albumState.filteredAlbums;
|
||||
final isLoading = albumState.isLoading;
|
||||
final error = albumState.error;
|
||||
final albums =
|
||||
ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
edgeOffset: 100,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
ImmichSliverAppBar(
|
||||
snap: false,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
@@ -132,14 +136,10 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
? _AlbumGrid(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
isLoading: isLoading,
|
||||
error: error,
|
||||
)
|
||||
: _AlbumList(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
isLoading: isLoading,
|
||||
error: error,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -478,48 +478,17 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumList extends StatelessWidget {
|
||||
class _AlbumList extends ConsumerWidget {
|
||||
const _AlbumList({
|
||||
required this.isLoading,
|
||||
required this.error,
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
'Error loading albums: $error',
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
@@ -567,9 +536,12 @@ class _AlbumList extends StatelessWidget {
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
onTap: () => context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
),
|
||||
onTap: () {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
},
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
@@ -619,44 +591,13 @@ class _AlbumGrid extends StatelessWidget {
|
||||
const _AlbumGrid({
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
required this.isLoading,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
'Error loading albums: $error',
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
@@ -692,7 +633,7 @@ class _AlbumGrid extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _GridAlbumCard extends StatelessWidget {
|
||||
class _GridAlbumCard extends ConsumerWidget {
|
||||
const _GridAlbumCard({
|
||||
required this.album,
|
||||
required this.userId,
|
||||
@@ -702,11 +643,14 @@ class _GridAlbumCard extends StatelessWidget {
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
),
|
||||
onTap: () {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceBright,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
|
||||
@@ -202,6 +203,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
||||
);
|
||||
|
||||
if (album != null) {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.replaceRoute(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user