Compare commits

..

1 Commits

Author SHA1 Message Date
Alex
36cffbd44a chore: attempt to handle database lock error when scrubbing the timeline 2025-07-17 11:39:06 -05:00
185 changed files with 1337 additions and 7521 deletions

View File

@@ -11,8 +11,8 @@ services:
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules - server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules - web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
database: database:

View File

@@ -13,8 +13,8 @@ services:
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules - open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules - server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules - web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
immich-web: immich-web:

View File

@@ -3,6 +3,9 @@
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh source /immich-devcontainer/container-common.sh
log "Setting up Immich dev container..."
fix_permissions
log "Installing npm dependencies (node_modules)..." log "Installing npm dependencies (node_modules)..."
install_dependencies install_dependencies

View File

@@ -1,41 +1,41 @@
.vscode/ .vscode/
.github/ .github/
.git/ .git/
.env*
*.log
*.tmp
*.temp
**/Dockerfile
**/node_modules/
**/.pnpm-store/
**/dist/
**/coverage/
**/build/
design/ design/
docker/ docker/
Dockerfile
!docker/scripts !docker/scripts
docs/ docs/
!docs/package.json !docs/package.json
!docs/package-lock.json !docs/package-lock.json
e2e/ e2e/
!e2e/package.json !e2e/package.json
!e2e/package-lock.json !e2e/package-lock.json
fastlane/ fastlane/
machine-learning/ machine-learning/
misc/ misc/
mobile/ mobile/
open-api/typescript-sdk/build/ cli/coverage/
!open-api/typescript-sdk/package.json cli/dist/
!open-api/typescript-sdk/package-lock.json cli/node_modules/
cli/Dockerfile
open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/
server/coverage/
server/node_modules/
server/upload/ server/upload/
server/src/queries server/src/queries
server/dist/
server/www/ server/www/
server/Dockerfile
web/node_modules/
web/coverage/
web/.svelte-kit web/.svelte-kit
web/build/
web/.env
web/Dockerfile

1
.gitignore vendored
View File

@@ -24,4 +24,3 @@ mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml mobile/ios/fastlane/report.xml
vite.config.js.timestamp-* vite.config.js.timestamp-*
.pnpm-store

View File

@@ -16,25 +16,22 @@ name: immich-dev
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: ['immich-dev'] command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest image: immich-server-dev:latest
# extends: # extends:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
args:
- SERVER_USER=${SERVER_USER:-0}
- SERVER_GROUP=${SERVER_GROUP:-0}
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
target: dev target: dev
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ../server:/usr/src/app/server - ../server:/usr/src/app
- ../open-api:/usr/src/app/open-api - ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/server/node_modules - /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
@@ -72,23 +69,19 @@ services:
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
# user: 0:0 # user: 0:0
build: build:
args: context: ../web
- WEB_USER=${WEB_USER:-1000} command: ['/usr/src/app/bin/immich-web']
- WEB_GROUP=${WEB_GROUP:-1000}
context: ../
dockerfile: web/Dockerfile
command: ['immich-web']
env_file: env_file:
- .env - .env
ports: ports:
- 3000:3000 - 3000:3000
- 24678:24678 - 24678:24678
volumes: volumes:
- ../web:/usr/src/app/web - ../web:/usr/src/app
- ../i18n:/usr/src/app/i18n - ../i18n:/usr/src/i18n
- ../open-api/:/usr/src/app/open-api/ - ../open-api/:/usr/src/open-api/
# - ../../ui:/usr/ui # - ../../ui:/usr/ui
- /usr/src/app/web/node_modules - /usr/src/app/node_modules
ulimits: ulimits:
nofile: nofile:
soft: 1048576 soft: 1048576

View File

@@ -2,17 +2,16 @@
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands: The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
| Command | Description | | Command | Description |
| ------------------------ | ------------------------------------------------------------- | | ------------------------ | ------------------------------------- |
| `help` | Display help | | `help` | Display help |
| `reset-admin-password` | Reset the password for the admin user | | `reset-admin-password` | Reset the password for the admin user |
| `disable-password-login` | Disable password login | | `disable-password-login` | Disable password login |
| `enable-password-login` | Enable password login | | `enable-password-login` | Enable password login |
| `enable-oauth-login` | Enable OAuth login | | `enable-oauth-login` | Enable OAuth login |
| `disable-oauth-login` | Disable OAuth login | | `disable-oauth-login` | Disable OAuth login |
| `list-users` | List Immich users | | `list-users` | List Immich users |
| `version` | Print Immich version | | `version` | Print Immich version |
| `change-media-location` | Change database file paths to align with a new media location |
## How to run a command ## How to run a command
@@ -89,24 +88,3 @@ Print Immich Version
immich-admin version immich-admin version
v1.129.0 v1.129.0
``` ```
Change media location
```
immich-admin change-media-location
? Enter the previous value of IMMICH_MEDIA_LOCATION: /usr/src/app/upload
? Enter the new value of IMMICH_MEDIA_LOCATION: /data
Previous value: /usr/src/app/upload
Current value: /data
Changing database paths from "/usr/src/app/upload/*" to "/data/*"
? Do you want to proceed? [Y/n] y
Database file paths updated successfully! 🎉
You may now set IMMICH_MEDIA_LOCATION=/data and restart!
(please remember to update applicable volume mounts e.g. ${UPLOAD_LOCATION}:/data)
```

View File

@@ -29,20 +29,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## General ## General
| Variable | Description | Default | Containers | Workers | | Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :---------------------------------: | :----------------------- | :----------------- | | :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices | | `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/usr/src/app/upload`<sup>\*3</sup> | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | | `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | | `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.

View File

@@ -3,6 +3,7 @@ name: immich-e2e
services: services:
immich-server: immich-server:
container_name: immich-e2e-server container_name: immich-e2e-server
command: ['./start.sh']
image: immich-server:latest image: immich-server:latest
build: build:
context: ../ context: ../

View File

@@ -373,7 +373,7 @@
"admin_password": "Admin Password", "admin_password": "Admin Password",
"administration": "Administration", "administration": "Administration",
"advanced": "Advanced", "advanced": "Advanced",
"advanced_settings_beta_timeline_subtitle": "Try the new app experience", "advanced_settings_beta_timeline_subtitle": "Try the new app experience.",
"advanced_settings_beta_timeline_title": "Beta Timeline", "advanced_settings_beta_timeline_title": "Beta Timeline",
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.", "advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
@@ -1693,7 +1693,6 @@
"settings_saved": "Settings saved", "settings_saved": "Settings saved",
"setup_pin_code": "Setup a PIN code", "setup_pin_code": "Setup a PIN code",
"share": "Share", "share": "Share",
"share_action_prompt": "Shared {count} assets",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_assets_selected": "{count} selected", "share_assets_selected": "{count} selected",
"share_dialog_preparing": "Preparing...", "share_dialog_preparing": "Preparing...",
@@ -1795,7 +1794,6 @@
"sort_title": "Title", "sort_title": "Title",
"source": "Source", "source": "Source",
"stack": "Stack", "stack": "Stack",
"stack_action_prompt": "{count} stacked",
"stack_duplicates": "Stack duplicates", "stack_duplicates": "Stack duplicates",
"stack_select_one_photo": "Select one main photo for the stack", "stack_select_one_photo": "Select one main photo for the stack",
"stack_selected_photos": "Stack selected photos", "stack_selected_photos": "Stack selected photos",
@@ -1906,7 +1904,6 @@
"unselect_all_duplicates": "Unselect all duplicates", "unselect_all_duplicates": "Unselect all duplicates",
"unselect_all_in": "Unselect all in {group}", "unselect_all_in": "Unselect all in {group}",
"unstack": "Un-stack", "unstack": "Un-stack",
"unstack_action_prompt": "{count} unstacked",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untagged": "Untagged", "untagged": "Untagged",
"up_next": "Up next", "up_next": "Up next",

View File

@@ -106,7 +106,6 @@ custom_lint:
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... - 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/sync_stream.service.dart # Making sure to comply with the type from database
- lib/domain/services/search.service.dart
# refactor # refactor
- lib/models/map/map_marker.model.dart - lib/models/map/map_marker.model.dart

View File

@@ -3,8 +3,6 @@ plugins {
id "kotlin-android" id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
id 'com.google.devtools.ksp' 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() def localProperties = new Properties()
@@ -47,10 +45,6 @@ android {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
buildFeatures {
compose true
}
defaultConfig { defaultConfig {
applicationId "app.alextran.immich" applicationId "app.alextran.immich"
minSdkVersion 26 minSdkVersion 26
@@ -111,8 +105,6 @@ dependencies {
def guava_version = '33.3.1-android' def guava_version = '33.3.1-android'
def glide_version = '4.16.0' def glide_version = '4.16.0'
def serialization_version = '1.8.1' 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.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
@@ -124,17 +116,6 @@ dependencies {
ksp "com.github.bumptech.glide:ksp:$glide_version" ksp "com.github.bumptech.glide:ksp:$glide_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' 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 // This is uncommented in F-Droid build script

View File

@@ -25,15 +25,8 @@
@com.google.gson.annotations.SerializedName <fields>; @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. # 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 com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends 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.** { *; }

View File

@@ -141,41 +141,6 @@
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
tools:node="remove" /> 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> </application>

View File

@@ -1,33 +0,0 @@
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
}

View File

@@ -1,241 +0,0 @@
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(AssetType.IMAGE)
val albumId = widgetConfig[kSelectedAlbum]
val showSubtitle = widgetConfig[kShowAlbumName]
val albumName = widgetConfig[kSelectedAlbumName]
var subtitle: String? = if (showSubtitle == true) albumName 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(AssetType.IMAGE))
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(AssetType.IMAGE, 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)
}
}
}

View File

@@ -1,103 +0,0 @@
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)
}
}

View File

@@ -1,56 +0,0 @@
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)
}
}
}
}

View File

@@ -1,124 +0,0 @@
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))
}
}
}
}
}
}
}
}

View File

@@ -1,55 +0,0 @@
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)
}
}
}
}

View File

@@ -1,64 +0,0 @@
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
)
}
}
}
}

View File

@@ -1,28 +0,0 @@
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
)
}

View File

@@ -1,210 +0,0 @@
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")) + 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)
)
}
}
}
}
}
}
}
}
}

View File

@@ -1,79 +0,0 @@
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()
)
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.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,9 +0,0 @@
<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"
/>

View File

@@ -1,13 +0,0 @@
<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"
/>

View File

@@ -1 +1 @@
version: '>=1.29.0 <=1.30.0' version: '>=1.29.0 <1.30.0'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase {
return rules; return rules;
} }
static LintCode makeCode(String name, LintOptions options) => LintCode( static makeCode(String name, LintOptions options) => LintCode(
name: name, name: name,
problemMessage: options.json["message"] as String, problemMessage: options.json["message"] as String,
errorSeverity: ErrorSeverity.WARNING, errorSeverity: ErrorSeverity.WARNING,

View File

@@ -0,0 +1 @@
{"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>"]}}}

View File

@@ -28,8 +28,7 @@ const String appShareGroupId = "group.app.immich.share";
// add widget identifiers here for new widgets // add widget identifiers here for new widgets
// these are used to force a widget refresh // these are used to force a widget refresh
// (iOSName, androidFQDN) const List<String> kWidgetNames = [
const List<(String, String)> kWidgetNames = [ 'com.immich.widget.random',
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'), 'com.immich.widget.memory',
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
]; ];

View File

@@ -45,7 +45,6 @@ class LocalAsset extends BaseAsset {
}'''; }''';
} }
// Not checking for remoteId here
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) return false;

View File

@@ -14,8 +14,6 @@ class RemoteAsset extends BaseAsset {
final String? thumbHash; final String? thumbHash;
final AssetVisibility visibility; final AssetVisibility visibility;
final String ownerId; final String ownerId;
final String? stackId;
final int stackCount;
const RemoteAsset({ const RemoteAsset({
required this.id, required this.id,
@@ -33,8 +31,6 @@ class RemoteAsset extends BaseAsset {
this.thumbHash, this.thumbHash,
this.visibility = AssetVisibility.timeline, this.visibility = AssetVisibility.timeline,
super.livePhotoVideoId, super.livePhotoVideoId,
this.stackId,
this.stackCount = 0,
}); });
@override @override
@@ -60,14 +56,9 @@ class RemoteAsset extends BaseAsset {
isFavorite: $isFavorite, isFavorite: $isFavorite,
thumbHash: ${thumbHash ?? "<NA>"}, thumbHash: ${thumbHash ?? "<NA>"},
visibility: $visibility, visibility: $visibility,
stackId: ${stackId ?? "<NA>"},
stackCount: $stackCount,
checksum: $checksum,
livePhotoVideoId: ${livePhotoVideoId ?? "<NA>"},
}'''; }''';
} }
// Not checking for localId here
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAsset) return false; if (other is! RemoteAsset) return false;
@@ -76,9 +67,7 @@ class RemoteAsset extends BaseAsset {
id == other.id && id == other.id &&
ownerId == other.ownerId && ownerId == other.ownerId &&
thumbHash == other.thumbHash && thumbHash == other.thumbHash &&
visibility == other.visibility && visibility == other.visibility;
stackId == other.stackId &&
stackCount == other.stackCount;
} }
@override @override
@@ -88,9 +77,7 @@ class RemoteAsset extends BaseAsset {
ownerId.hashCode ^ ownerId.hashCode ^
localId.hashCode ^ localId.hashCode ^
thumbHash.hashCode ^ thumbHash.hashCode ^
visibility.hashCode ^ visibility.hashCode;
stackId.hashCode ^
stackCount.hashCode;
RemoteAsset copyWith({ RemoteAsset copyWith({
String? id, String? id,
@@ -108,8 +95,6 @@ class RemoteAsset extends BaseAsset {
String? thumbHash, String? thumbHash,
AssetVisibility? visibility, AssetVisibility? visibility,
String? livePhotoVideoId, String? livePhotoVideoId,
String? stackId,
int? stackCount,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -127,8 +112,6 @@ class RemoteAsset extends BaseAsset {
thumbHash: thumbHash ?? this.thumbHash, thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId,
stackCount: stackCount ?? this.stackCount,
); );
} }
} }

View File

@@ -124,21 +124,7 @@ class DriftMemory {
@override @override
String toString() { String toString() {
return '''Memory { 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)';
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 @override

View File

@@ -1,8 +1,7 @@
import 'dart:convert'; import 'dart:convert';
// TODO: Remove PersonDto once Isar is removed class Person {
class PersonDto { const Person({
const PersonDto({
required this.id, required this.id,
this.birthDate, this.birthDate,
required this.isHidden, required this.isHidden,
@@ -23,7 +22,7 @@ class PersonDto {
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
} }
PersonDto copyWith({ Person copyWith({
String? id, String? id,
DateTime? birthDate, DateTime? birthDate,
bool? isHidden, bool? isHidden,
@@ -31,7 +30,7 @@ class PersonDto {
String? thumbnailPath, String? thumbnailPath,
DateTime? updatedAt, DateTime? updatedAt,
}) { }) {
return PersonDto( return Person(
id: id ?? this.id, id: id ?? this.id,
birthDate: birthDate ?? this.birthDate, birthDate: birthDate ?? this.birthDate,
isHidden: isHidden ?? this.isHidden, isHidden: isHidden ?? this.isHidden,
@@ -52,8 +51,8 @@ class PersonDto {
}; };
} }
factory PersonDto.fromMap(Map<String, dynamic> map) { factory Person.fromMap(Map<String, dynamic> map) {
return PersonDto( return Person(
id: map['id'] as String, id: map['id'] as String,
birthDate: map['birthDate'] != null birthDate: map['birthDate'] != null
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
@@ -69,11 +68,11 @@ class PersonDto {
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory PersonDto.fromJson(String source) => factory Person.fromJson(String source) =>
PersonDto.fromMap(json.decode(source) as Map<String, dynamic>); Person.fromMap(json.decode(source) as Map<String, dynamic>);
@override @override
bool operator ==(covariant PersonDto other) { bool operator ==(covariant Person other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.id == id && return other.id == id &&
@@ -94,109 +93,3 @@ class PersonDto {
updatedAt.hashCode; 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;
}
}

View File

@@ -1,38 +0,0 @@
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;
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
// Model for a stack stored in the server // Model for a stack stored in the server
class Stack { class Stack {
final String id; final String id;
@@ -30,15 +32,34 @@ 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 @override
String toString() { String toString() {
return '''Stack { return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
id: $id,
createdAt: $createdAt,
updatedAt: $updatedAt,
ownerId: $ownerId,
primaryAssetId: $primaryAssetId
}''';
} }
@override @override
@@ -61,27 +82,3 @@ class Stack {
primaryAssetId.hashCode; 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;
}

View File

@@ -1,5 +1,3 @@
import 'package:immich_mobile/domain/utils/event_stream.dart';
enum GroupAssetsBy { enum GroupAssetsBy {
day, day,
month, month,
@@ -40,7 +38,3 @@ class TimeBucket extends Bucket {
@override @override
int get hashCode => super.hashCode ^ date.hashCode; int get hashCode => super.hashCode ^ date.hashCode;
} }
class TimelineReloadEvent extends Event {
const TimelineReloadEvent();
}

View File

@@ -24,17 +24,6 @@ class AssetService {
: _remoteAssetRepository.watchAsset(id); : _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 { Future<ExifInfo?> getExif(BaseAsset asset) async {
if (!asset.hasRemote) { if (!asset.hasRemote) {
return null; return null;

View File

@@ -1,92 +0,0 @@
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'),
};
}

View File

@@ -240,10 +240,6 @@ class SyncStreamService {
return _syncStreamRepository.deleteUserMetadatasV1( return _syncStreamRepository.deleteUserMetadatasV1(
data.cast(), data.cast(),
); );
case SyncEntityType.personV1:
return _syncStreamRepository.updatePeopleV1(data.cast());
case SyncEntityType.personDeleteV1:
return _syncStreamRepository.deletePeopleV1(data.cast());
default: default:
_logger.warning("Unknown sync data type: $type"); _logger.warning("Unknown sync data type: $type");
} }

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart';
@@ -65,9 +66,6 @@ class TimelineFactory {
TimelineService place(String place) => TimelineService place(String place) =>
TimelineService(_timelineRepository.place(place, groupBy)); TimelineService(_timelineRepository.place(place, groupBy));
TimelineService fromAssets(List<BaseAsset> assets) =>
TimelineService(_timelineRepository.fromAssets(assets));
} }
class TimelineService { class TimelineService {
@@ -115,8 +113,19 @@ class TimelineService {
totalAssets - _bufferOffset, totalAssets - _bufferOffset,
); );
} }
_buffer = await _assetSource(offset, count);
_bufferOffset = offset; try {
_buffer = await _assetSource(offset, count);
_bufferOffset = offset;
} catch (e) {
if (e.toString().contains('database has been locked')) {
debugPrint(
"TimelineService::loadAssets - Database locked, returning cached assets",
);
return;
}
rethrow;
}
} }
// change the state's total assets count only after the buffer is reloaded // change the state's total assets count only after the buffer is reloaded
@@ -156,8 +165,22 @@ class TimelineService {
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len), : (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
); );
_buffer = await _assetSource(start, len); try {
_bufferOffset = start; _buffer = await _assetSource(start, len);
_bufferOffset = start;
} catch (e) {
if (e.toString().contains('database has been locked') &&
_buffer.isNotEmpty) {
debugPrint(
"TimelineService::loadAssets - Database locked, returning cached assets",
);
if (hasRange(index, count)) {
return getAssets(index, count);
}
return <BaseAsset>[];
}
rethrow;
}
return getAssets(index, count); return getAssets(index, count);
} }

View File

@@ -4,24 +4,13 @@ import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart'; import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart'; import 'package:worker_manager/worker_manager.dart';
typedef SyncCallback = void Function();
typedef SyncErrorCallback = void Function(String error);
class BackgroundSyncManager { class BackgroundSyncManager {
final SyncCallback? onRemoteSyncStart;
final SyncCallback? onRemoteSyncComplete;
final SyncErrorCallback? onRemoteSyncError;
Cancelable<void>? _syncTask; Cancelable<void>? _syncTask;
Cancelable<void>? _syncWebsocketTask; Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _deviceAlbumSyncTask; Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _hashTask; Cancelable<void>? _hashTask;
BackgroundSyncManager({ BackgroundSyncManager();
this.onRemoteSyncStart,
this.onRemoteSyncComplete,
this.onRemoteSyncError,
});
Future<void> cancel() { Future<void> cancel() {
final futures = <Future>[]; final futures = <Future>[];
@@ -83,16 +72,10 @@ class BackgroundSyncManager {
return _syncTask!.future; return _syncTask!.future;
} }
onRemoteSyncStart?.call();
_syncTask = runInIsolateGentle( _syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(), computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
); );
return _syncTask!.whenComplete(() { return _syncTask!.whenComplete(() {
onRemoteSyncComplete?.call();
_syncTask = null;
}).catchError((error) {
onRemoteSyncError?.call(error.toString());
_syncTask = null; _syncTask = null;
}); });
} }

View File

@@ -1,9 +1,17 @@
import 'dart:async'; import 'dart:async';
class Event { sealed class Event {
const Event(); const Event();
} }
class TimelineReloadEvent extends Event {
const TimelineReloadEvent();
}
class ViewerOpenBottomSheetEvent extends Event {
const ViewerOpenBottomSheetEvent();
}
class EventStream { class EventStream {
EventStream._(); EventStream._();

View File

@@ -1,6 +1,5 @@
import 'remote_asset.entity.dart'; import 'remote_asset.entity.dart';
import 'local_asset.entity.dart'; import 'local_asset.entity.dart';
import 'stack.entity.dart';
mergedAsset: SELECT * FROM mergedAsset: SELECT * FROM
( (
@@ -19,33 +18,13 @@ mergedAsset: SELECT * FROM
rae.checksum, rae.checksum,
rae.owner_id, rae.owner_id,
rae.live_photo_video_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 FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum 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 WHERE
rae.deleted_at IS NULL rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
AND rae.visibility = 0
AND rae.owner_id in ?
AND (
rae.stack_id IS NULL
OR rae.id = se.primary_asset_id
)
UNION ALL UNION ALL
SELECT SELECT
NULL as remote_id, NULL as remote_id,
@@ -62,9 +41,7 @@ mergedAsset: SELECT * FROM
lae.checksum, lae.checksum,
NULL as owner_id, NULL as owner_id,
NULL as live_photo_video_id, NULL as live_photo_video_id,
lae.orientation, lae.orientation
NULL as stack_id,
0 AS stack_count
FROM FROM
local_asset_entity lae local_asset_entity lae
LEFT JOIN LEFT JOIN
@@ -91,16 +68,8 @@ FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum local_asset_entity lae ON rae.checksum = lae.checksum
LEFT JOIN
stack_entity se ON rae.stack_id = se.id
WHERE WHERE
rae.deleted_at IS NULL rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
AND rae.visibility = 0
AND rae.owner_id in ?
AND (
rae.stack_id IS NULL
OR rae.id = se.primary_asset_id
)
UNION ALL UNION ALL
SELECT SELECT
lae.name, lae.name,

View File

@@ -7,8 +7,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i3; as i3;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i4; as i4;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i5;
class MergedAssetDrift extends i1.ModularAccessor { class MergedAssetDrift extends i1.ModularAccessor {
MergedAssetDrift(i0.GeneratedDatabase db) : super(db); MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
@@ -20,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex); final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( 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, 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}', '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}',
variables: [ variables: [
for (var $ in var1) i0.Variable<String>($), for (var $ in var1) i0.Variable<String>($),
...generatedlimit.introducedVariables ...generatedlimit.introducedVariables
@@ -28,7 +26,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
readsFrom: { readsFrom: {
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
stackEntity,
...generatedlimit.watchedTables, ...generatedlimit.watchedTables,
}).map((i0.QueryRow row) => MergedAssetResult( }).map((i0.QueryRow row) => MergedAssetResult(
remoteId: row.readNullable<String>('remote_id'), remoteId: row.readNullable<String>('remote_id'),
@@ -47,8 +44,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
ownerId: row.readNullable<String>('owner_id'), ownerId: row.readNullable<String>('owner_id'),
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'), livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'), orientation: row.read<int>('orientation'),
stackId: row.readNullable<String>('stack_id'),
stackCount: row.read<int>('stack_count'),
)); ));
} }
@@ -58,7 +53,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandedvar2 = $expandVar($arrayStartIndex, var2.length); final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
$arrayStartIndex += var2.length; $arrayStartIndex += var2.length;
return customSelect( 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 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', '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',
variables: [ variables: [
i0.Variable<int>(groupBy), i0.Variable<int>(groupBy),
for (var $ in var2) i0.Variable<String>($) for (var $ in var2) i0.Variable<String>($)
@@ -66,7 +61,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
readsFrom: { readsFrom: {
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
stackEntity,
}).map((i0.QueryRow row) => MergedBucketResult( }).map((i0.QueryRow row) => MergedBucketResult(
assetCount: row.read<int>('asset_count'), assetCount: row.read<int>('asset_count'),
bucketDate: row.read<String>('bucket_date'), bucketDate: row.read<String>('bucket_date'),
@@ -79,9 +73,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
i4.$LocalAssetEntityTable get localAssetEntity => i4.$LocalAssetEntityTable get localAssetEntity =>
i1.ReadDatabaseContainer(attachedDatabase) i1.ReadDatabaseContainer(attachedDatabase)
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity'); .resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
i5.$StackEntityTable get stackEntity =>
i1.ReadDatabaseContainer(attachedDatabase)
.resultSet<i5.$StackEntityTable>('stack_entity');
} }
class MergedAssetResult { class MergedAssetResult {
@@ -100,8 +91,6 @@ class MergedAssetResult {
final String? ownerId; final String? ownerId;
final String? livePhotoVideoId; final String? livePhotoVideoId;
final int orientation; final int orientation;
final String? stackId;
final int stackCount;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
this.localId, this.localId,
@@ -118,8 +107,6 @@ class MergedAssetResult {
this.ownerId, this.ownerId,
this.livePhotoVideoId, this.livePhotoVideoId,
required this.orientation, required this.orientation,
this.stackId,
required this.stackCount,
}); });
} }

View File

@@ -1,34 +0,0 @@
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};
}

View File

@@ -1,933 +0,0 @@
// 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();
}
}

View File

@@ -34,8 +34,6 @@ class RemoteAssetEntity extends Table
IntColumn get visibility => intEnum<AssetVisibility>()(); IntColumn get visibility => intEnum<AssetVisibility>()();
TextColumn get stackId => text().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -57,6 +55,5 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
visibility: visibility, visibility: visibility,
livePhotoVideoId: livePhotoVideoId, livePhotoVideoId: livePhotoVideoId,
localId: null, localId: null,
stackId: stackId,
); );
} }

View File

@@ -29,7 +29,6 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId, i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
i0.Value<String?> stackId,
}); });
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
= i1.RemoteAssetEntityCompanion Function({ = i1.RemoteAssetEntityCompanion Function({
@@ -49,7 +48,6 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId, i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility, i0.Value<i2.AssetVisibility> visibility,
i0.Value<String?> stackId,
}); });
final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences<
@@ -147,9 +145,6 @@ class $$RemoteAssetEntityTableFilterComposer
column: $table.visibility, column: $table.visibility,
builder: (column) => i0.ColumnWithTypeConverterFilters(column)); builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<String> get stackId => $composableBuilder(
column: $table.stackId, builder: (column) => i0.ColumnFilters(column));
i5.$$UserEntityTableFilterComposer get ownerId { i5.$$UserEntityTableFilterComposer get ownerId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -236,9 +231,6 @@ class $$RemoteAssetEntityTableOrderingComposer
column: $table.visibility, column: $table.visibility,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get stackId => $composableBuilder(
column: $table.stackId, builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get ownerId { i5.$$UserEntityTableOrderingComposer get ownerId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -317,9 +309,6 @@ class $$RemoteAssetEntityTableAnnotationComposer
$composableBuilder( $composableBuilder(
column: $table.visibility, builder: (column) => column); column: $table.visibility, builder: (column) => column);
i0.GeneratedColumn<String> get stackId =>
$composableBuilder(column: $table.stackId, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get ownerId { i5.$$UserEntityTableAnnotationComposer get ownerId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
@@ -384,7 +373,6 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(), i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(), i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
i0.Value<String?> stackId = const i0.Value.absent(),
}) => }) =>
i1.RemoteAssetEntityCompanion( i1.RemoteAssetEntityCompanion(
name: name, name: name,
@@ -403,7 +391,6 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId, livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
stackId: stackId,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String name, required String name,
@@ -422,7 +409,6 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(), i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
}) => }) =>
i1.RemoteAssetEntityCompanion.insert( i1.RemoteAssetEntityCompanion.insert(
name: name, name: name,
@@ -441,7 +427,6 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId, livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
stackId: stackId,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => ( .map((e) => (
@@ -617,12 +602,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.int, requiredDuringInsert: true) type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.AssetVisibility>( .withConverter<i2.AssetVisibility>(
i1.$RemoteAssetEntityTable.$convertervisibility); 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 @override
List<i0.GeneratedColumn> get $columns => [ List<i0.GeneratedColumn> get $columns => [
name, name,
@@ -640,8 +619,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId, livePhotoVideoId,
visibility, visibility
stackId
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -725,10 +703,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
livePhotoVideoId.isAcceptableOrUnknown( livePhotoVideoId.isAcceptableOrUnknown(
data['live_photo_video_id']!, _livePhotoVideoIdMeta)); data['live_photo_video_id']!, _livePhotoVideoIdMeta));
} }
if (data.containsKey('stack_id')) {
context.handle(_stackIdMeta,
stackId.isAcceptableOrUnknown(data['stack_id']!, _stackIdMeta));
}
return context; return context;
} }
@@ -774,8 +748,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
attachedDatabase.typeMapping.read( attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
stackId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}stack_id']),
); );
} }
@@ -813,7 +785,6 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? deletedAt; final DateTime? deletedAt;
final String? livePhotoVideoId; final String? livePhotoVideoId;
final i2.AssetVisibility visibility; final i2.AssetVisibility visibility;
final String? stackId;
const RemoteAssetEntityData( const RemoteAssetEntityData(
{required this.name, {required this.name,
required this.type, required this.type,
@@ -830,8 +801,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.thumbHash, this.thumbHash,
this.deletedAt, this.deletedAt,
this.livePhotoVideoId, this.livePhotoVideoId,
required this.visibility, required this.visibility});
this.stackId});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{}; final map = <String, i0.Expression>{};
@@ -871,9 +841,6 @@ class RemoteAssetEntityData extends i0.DataClass
map['visibility'] = i0.Variable<int>( map['visibility'] = i0.Variable<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
} }
if (!nullToAbsent || stackId != null) {
map['stack_id'] = i0.Variable<String>(stackId);
}
return map; return map;
} }
@@ -899,7 +866,6 @@ class RemoteAssetEntityData extends i0.DataClass
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']), livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility visibility: i1.$RemoteAssetEntityTable.$convertervisibility
.fromJson(serializer.fromJson<int>(json['visibility'])), .fromJson(serializer.fromJson<int>(json['visibility'])),
stackId: serializer.fromJson<String?>(json['stackId']),
); );
} }
@override @override
@@ -924,7 +890,6 @@ class RemoteAssetEntityData extends i0.DataClass
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId), 'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>( 'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
'stackId': serializer.toJson<String?>(stackId),
}; };
} }
@@ -944,8 +909,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = 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( i1.RemoteAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -968,7 +932,6 @@ class RemoteAssetEntityData extends i0.DataClass
? livePhotoVideoId.value ? livePhotoVideoId.value
: this.livePhotoVideoId, : this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
stackId: stackId.present ? stackId.value : this.stackId,
); );
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
return RemoteAssetEntityData( return RemoteAssetEntityData(
@@ -996,7 +959,6 @@ class RemoteAssetEntityData extends i0.DataClass
: this.livePhotoVideoId, : this.livePhotoVideoId,
visibility: visibility:
data.visibility.present ? data.visibility.value : this.visibility, data.visibility.present ? data.visibility.value : this.visibility,
stackId: data.stackId.present ? data.stackId.value : this.stackId,
); );
} }
@@ -1018,8 +980,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ') ..write('visibility: $visibility')
..write('stackId: $stackId')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -1041,8 +1002,7 @@ class RemoteAssetEntityData extends i0.DataClass
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId, livePhotoVideoId,
visibility, visibility);
stackId);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -1062,8 +1022,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.thumbHash == this.thumbHash && other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt && other.deletedAt == this.deletedAt &&
other.livePhotoVideoId == this.livePhotoVideoId && other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility && other.visibility == this.visibility);
other.stackId == this.stackId);
} }
class RemoteAssetEntityCompanion class RemoteAssetEntityCompanion
@@ -1084,7 +1043,6 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime?> deletedAt; final i0.Value<DateTime?> deletedAt;
final i0.Value<String?> livePhotoVideoId; final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility; final i0.Value<i2.AssetVisibility> visibility;
final i0.Value<String?> stackId;
const RemoteAssetEntityCompanion({ const RemoteAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(), this.type = const i0.Value.absent(),
@@ -1102,7 +1060,6 @@ class RemoteAssetEntityCompanion
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(), this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(), this.visibility = const i0.Value.absent(),
this.stackId = const i0.Value.absent(),
}); });
RemoteAssetEntityCompanion.insert({ RemoteAssetEntityCompanion.insert({
required String name, required String name,
@@ -1121,7 +1078,6 @@ class RemoteAssetEntityCompanion
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(), this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
this.stackId = const i0.Value.absent(),
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
id = i0.Value(id), id = i0.Value(id),
@@ -1145,7 +1101,6 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? deletedAt, i0.Expression<DateTime>? deletedAt,
i0.Expression<String>? livePhotoVideoId, i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility, i0.Expression<int>? visibility,
i0.Expression<String>? stackId,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (name != null) 'name': name, if (name != null) 'name': name,
@@ -1164,7 +1119,6 @@ class RemoteAssetEntityCompanion
if (deletedAt != null) 'deleted_at': deletedAt, if (deletedAt != null) 'deleted_at': deletedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility, if (visibility != null) 'visibility': visibility,
if (stackId != null) 'stack_id': stackId,
}); });
} }
@@ -1184,8 +1138,7 @@ class RemoteAssetEntityCompanion
i0.Value<String?>? thumbHash, i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt, i0.Value<DateTime?>? deletedAt,
i0.Value<String?>? livePhotoVideoId, i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility, i0.Value<i2.AssetVisibility>? visibility}) {
i0.Value<String?>? stackId}) {
return i1.RemoteAssetEntityCompanion( return i1.RemoteAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -1203,7 +1156,6 @@ class RemoteAssetEntityCompanion
deletedAt: deletedAt ?? this.deletedAt, deletedAt: deletedAt ?? this.deletedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
stackId: stackId ?? this.stackId,
); );
} }
@@ -1261,9 +1213,6 @@ class RemoteAssetEntityCompanion
.$RemoteAssetEntityTable.$convertervisibility .$RemoteAssetEntityTable.$convertervisibility
.toSql(visibility.value)); .toSql(visibility.value));
} }
if (stackId.present) {
map['stack_id'] = i0.Variable<String>(stackId.value);
}
return map; return map;
} }
@@ -1285,8 +1234,7 @@ class RemoteAssetEntityCompanion
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ') ..write('visibility: $visibility')
..write('stackId: $stackId')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@@ -11,7 +11,6 @@ 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.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.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/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.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
@@ -55,7 +54,6 @@ class IsarDatabaseRepository implements IDatabaseRepository {
MemoryEntity, MemoryEntity,
MemoryAssetEntity, MemoryAssetEntity,
StackEntity, StackEntity,
PersonEntity,
], ],
include: { include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift', 'package:immich_mobile/infrastructure/entities/merged_asset.drift',

View File

@@ -7,33 +7,31 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i2; as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i3; as i3;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i5; as i4;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i6; as i5;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i7; as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i8; as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i9; as i8;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i10; as i9;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i11; as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i12; as i11;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i13; as i12;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i14; as i14;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i15;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i16; as i15;
import 'package:drift/internal/modular.dart' as i17; import 'package:drift/internal/modular.dart' as i16;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@@ -43,29 +41,28 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.$RemoteAssetEntityTable(this); i2.$RemoteAssetEntityTable(this);
late final i3.$LocalAssetEntityTable localAssetEntity = late final i3.$LocalAssetEntityTable localAssetEntity =
i3.$LocalAssetEntityTable(this); i3.$LocalAssetEntityTable(this);
late final i4.$StackEntityTable stackEntity = i4.$StackEntityTable(this); late final i4.$UserMetadataEntityTable userMetadataEntity =
late final i5.$UserMetadataEntityTable userMetadataEntity = i4.$UserMetadataEntityTable(this);
i5.$UserMetadataEntityTable(this); late final i5.$PartnerEntityTable partnerEntity =
late final i6.$PartnerEntityTable partnerEntity = i5.$PartnerEntityTable(this);
i6.$PartnerEntityTable(this); late final i6.$LocalAlbumEntityTable localAlbumEntity =
late final i7.$LocalAlbumEntityTable localAlbumEntity = i6.$LocalAlbumEntityTable(this);
i7.$LocalAlbumEntityTable(this); late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7.$LocalAlbumAssetEntityTable(this);
i8.$LocalAlbumAssetEntityTable(this); late final i8.$RemoteExifEntityTable remoteExifEntity =
late final i9.$RemoteExifEntityTable remoteExifEntity = i8.$RemoteExifEntityTable(this);
i9.$RemoteExifEntityTable(this); late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = i9.$RemoteAlbumEntityTable(this);
i10.$RemoteAlbumEntityTable(this); late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i10.$RemoteAlbumAssetEntityTable(this);
i11.$RemoteAlbumAssetEntityTable(this); late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i11.$RemoteAlbumUserEntityTable(this);
i12.$RemoteAlbumUserEntityTable(this); late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this); late final i13.$MemoryAssetEntityTable memoryAssetEntity =
late final i14.$MemoryAssetEntityTable memoryAssetEntity = i13.$MemoryAssetEntityTable(this);
i14.$MemoryAssetEntityTable(this); late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this); i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this) .accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
.accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -74,7 +71,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
userEntity, userEntity,
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
stackEntity,
i3.idxLocalAssetChecksum, i3.idxLocalAssetChecksum,
i2.uQRemoteAssetOwnerChecksum, i2.uQRemoteAssetOwnerChecksum,
i2.idxRemoteAssetChecksum, i2.idxRemoteAssetChecksum,
@@ -88,7 +84,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAlbumUserEntity, remoteAlbumUserEntity,
memoryEntity, memoryEntity,
memoryAssetEntity, memoryAssetEntity,
personEntity stackEntity
]; ];
@override @override
i0.StreamQueryUpdateRules get streamUpdateRules => i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -101,13 +97,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), 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( i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity', on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete), limitUpdateKind: i0.UpdateKind.delete),
@@ -224,7 +213,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
on: i0.TableUpdateQuery.onTableName('user_entity', on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete), limitUpdateKind: i0.UpdateKind.delete),
result: [ result: [
i0.TableUpdate('person_entity', kind: i0.UpdateKind.delete), i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
], ],
), ),
], ],
@@ -243,29 +232,27 @@ class $DriftManager {
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i3.$$LocalAssetEntityTableTableManager get localAssetEntity => i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i4.$$StackEntityTableTableManager get stackEntity => i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i4.$$StackEntityTableTableManager(_db, _db.stackEntity); i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i5.$$UserMetadataEntityTableTableManager get userMetadataEntity => i5.$$PartnerEntityTableTableManager get partnerEntity =>
i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i6.$$PartnerEntityTableTableManager get partnerEntity => i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity => i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity => i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i11.$$RemoteAlbumAssetEntityTableTableManager( i10.$$RemoteAlbumAssetEntityTableTableManager(
_db, _db.remoteAlbumAssetEntity); _db, _db.remoteAlbumAssetEntity);
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12 i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i13.$$MemoryEntityTableTableManager get memoryEntity => i12.$$MemoryEntityTableTableManager get memoryEntity =>
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i15.$$PersonEntityTableTableManager get personEntity => i14.$$StackEntityTableTableManager get stackEntity =>
i15.$$PersonEntityTableTableManager(_db, _db.personEntity); i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
} }

View File

@@ -12,7 +12,6 @@ final class Schema2 extends i0.VersionedSchema {
userEntity, userEntity,
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
stackEntity,
idxLocalAssetChecksum, idxLocalAssetChecksum,
uQRemoteAssetOwnerChecksum, uQRemoteAssetOwnerChecksum,
idxRemoteAssetChecksum, idxRemoteAssetChecksum,
@@ -26,7 +25,7 @@ final class Schema2 extends i0.VersionedSchema {
remoteAlbumUserEntity, remoteAlbumUserEntity,
memoryEntity, memoryEntity,
memoryAssetEntity, memoryAssetEntity,
personEntity, stackEntity,
]; ];
late final Shape0 userEntity = Shape0( late final Shape0 userEntity = Shape0(
source: i0.VersionedTable( source: i0.VersionedTable(
@@ -74,7 +73,6 @@ final class Schema2 extends i0.VersionedSchema {
_column_18, _column_18,
_column_19, _column_19,
_column_20, _column_20,
_column_21,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
@@ -96,27 +94,9 @@ final class Schema2 extends i0.VersionedSchema {
_column_11, _column_11,
_column_12, _column_12,
_column_0, _column_0,
_column_22, _column_21,
_column_14, _column_14,
_column_23, _column_22,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_24,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
@@ -128,7 +108,7 @@ final class Schema2 extends i0.VersionedSchema {
'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)');
final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum', final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum',
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
late final Shape4 userMetadataEntity = Shape4( late final Shape3 userMetadataEntity = Shape3(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'user_metadata_entity', entityName: 'user_metadata_entity',
withoutRowId: true, withoutRowId: true,
@@ -137,14 +117,14 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(user_id, "key")', 'PRIMARY KEY(user_id, "key")',
], ],
columns: [ columns: [
_column_23,
_column_24,
_column_25, _column_25,
_column_26,
_column_27,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape5 partnerEntity = Shape5( late final Shape4 partnerEntity = Shape4(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'partner_entity', entityName: 'partner_entity',
withoutRowId: true, withoutRowId: true,
@@ -153,14 +133,14 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(shared_by_id, shared_with_id)', 'PRIMARY KEY(shared_by_id, shared_with_id)',
], ],
columns: [ columns: [
_column_26,
_column_27,
_column_28, _column_28,
_column_29,
_column_30,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape6 localAlbumEntity = Shape6( late final Shape5 localAlbumEntity = Shape5(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'local_album_entity', entityName: 'local_album_entity',
withoutRowId: true, withoutRowId: true,
@@ -172,14 +152,14 @@ final class Schema2 extends i0.VersionedSchema {
_column_0, _column_0,
_column_1, _column_1,
_column_5, _column_5,
_column_29,
_column_30,
_column_31, _column_31,
_column_32,
_column_33,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape7 localAlbumAssetEntity = Shape7( late final Shape6 localAlbumAssetEntity = Shape6(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'local_album_asset_entity', entityName: 'local_album_asset_entity',
withoutRowId: true, withoutRowId: true,
@@ -188,13 +168,13 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(asset_id, album_id)', 'PRIMARY KEY(asset_id, album_id)',
], ],
columns: [ columns: [
_column_34, _column_32,
_column_35, _column_33,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape8 remoteExifEntity = Shape8( late final Shape7 remoteExifEntity = Shape7(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'remote_exif_entity', entityName: 'remote_exif_entity',
withoutRowId: true, withoutRowId: true,
@@ -203,14 +183,16 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(asset_id)', 'PRIMARY KEY(asset_id)',
], ],
columns: [ columns: [
_column_34,
_column_35,
_column_36, _column_36,
_column_37, _column_37,
_column_38, _column_38,
_column_39, _column_39,
_column_40,
_column_41,
_column_11, _column_11,
_column_10, _column_10,
_column_40,
_column_41,
_column_42, _column_42,
_column_43, _column_43,
_column_44, _column_44,
@@ -223,13 +205,11 @@ final class Schema2 extends i0.VersionedSchema {
_column_51, _column_51,
_column_52, _column_52,
_column_53, _column_53,
_column_54,
_column_55,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape9 remoteAlbumEntity = Shape9( late final Shape8 remoteAlbumEntity = Shape8(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'remote_album_entity', entityName: 'remote_album_entity',
withoutRowId: true, withoutRowId: true,
@@ -240,18 +220,18 @@ final class Schema2 extends i0.VersionedSchema {
columns: [ columns: [
_column_0, _column_0,
_column_1, _column_1,
_column_56, _column_54,
_column_9, _column_9,
_column_5, _column_5,
_column_15, _column_15,
_column_55,
_column_56,
_column_57, _column_57,
_column_58,
_column_59,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape7 remoteAlbumAssetEntity = Shape7( late final Shape6 remoteAlbumAssetEntity = Shape6(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'remote_album_asset_entity', entityName: 'remote_album_asset_entity',
withoutRowId: true, withoutRowId: true,
@@ -260,13 +240,13 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(asset_id, album_id)', 'PRIMARY KEY(asset_id, album_id)',
], ],
columns: [ columns: [
_column_36, _column_34,
_column_60, _column_58,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape10 remoteAlbumUserEntity = Shape10( late final Shape9 remoteAlbumUserEntity = Shape9(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'remote_album_user_entity', entityName: 'remote_album_user_entity',
withoutRowId: true, withoutRowId: true,
@@ -275,14 +255,14 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(album_id, user_id)', 'PRIMARY KEY(album_id, user_id)',
], ],
columns: [ columns: [
_column_60, _column_58,
_column_25, _column_23,
_column_61, _column_59,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape11 memoryEntity = Shape11( late final Shape10 memoryEntity = Shape10(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'memory_entity', entityName: 'memory_entity',
withoutRowId: true, withoutRowId: true,
@@ -297,17 +277,17 @@ final class Schema2 extends i0.VersionedSchema {
_column_18, _column_18,
_column_15, _column_15,
_column_8, _column_8,
_column_60,
_column_61,
_column_62, _column_62,
_column_63, _column_63,
_column_64, _column_64,
_column_65, _column_65,
_column_66,
_column_67,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape12 memoryAssetEntity = Shape12( late final Shape11 memoryAssetEntity = Shape11(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'memory_asset_entity', entityName: 'memory_asset_entity',
withoutRowId: true, withoutRowId: true,
@@ -316,15 +296,15 @@ final class Schema2 extends i0.VersionedSchema {
'PRIMARY KEY(asset_id, memory_id)', 'PRIMARY KEY(asset_id, memory_id)',
], ],
columns: [ columns: [
_column_36, _column_34,
_column_68, _column_66,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
alias: null); alias: null);
late final Shape13 personEntity = Shape13( late final Shape12 stackEntity = Shape12(
source: i0.VersionedTable( source: i0.VersionedTable(
entityName: 'person_entity', entityName: 'stack_entity',
withoutRowId: true, withoutRowId: true,
isStrict: true, isStrict: true,
tableConstraints: [ tableConstraints: [
@@ -335,13 +315,7 @@ final class Schema2 extends i0.VersionedSchema {
_column_9, _column_9,
_column_5, _column_5,
_column_15, _column_15,
_column_1, _column_67,
_column_69,
_column_70,
_column_71,
_column_72,
_column_73,
_column_74,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
@@ -431,8 +405,6 @@ class Shape1 extends i0.VersionedTable {
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>; columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get visibility => i1.GeneratedColumn<int> get visibility =>
columnsByName['visibility']! as i1.GeneratedColumn<int>; columnsByName['visibility']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get stackId =>
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
} }
i1.GeneratedColumn<int> _column_8(String aliasedName) => i1.GeneratedColumn<int> _column_8(String aliasedName) =>
@@ -480,9 +452,6 @@ i1.GeneratedColumn<String> _column_19(String aliasedName) =>
i1.GeneratedColumn<int> _column_20(String aliasedName) => i1.GeneratedColumn<int> _column_20(String aliasedName) =>
i1.GeneratedColumn<int>('visibility', aliasedName, false, i1.GeneratedColumn<int>('visibility', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_21(String aliasedName) =>
i1.GeneratedColumn<String>('stack_id', aliasedName, true,
type: i1.DriftSqlType.string);
class Shape2 extends i0.VersionedTable { class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased(); Shape2({required super.source, required super.alias}) : super.aliased();
@@ -510,35 +479,15 @@ class Shape2 extends i0.VersionedTable {
columnsByName['orientation']! as i1.GeneratedColumn<int>; columnsByName['orientation']! as i1.GeneratedColumn<int>;
} }
i1.GeneratedColumn<String> _column_22(String aliasedName) => i1.GeneratedColumn<String> _column_21(String aliasedName) =>
i1.GeneratedColumn<String>('checksum', aliasedName, true, i1.GeneratedColumn<String>('checksum', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_23(String aliasedName) => i1.GeneratedColumn<int> _column_22(String aliasedName) =>
i1.GeneratedColumn<int>('orientation', aliasedName, false, i1.GeneratedColumn<int>('orientation', aliasedName, false,
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0')); type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
class Shape3 extends i0.VersionedTable { class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased(); Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get primaryAssetId =>
columnsByName['primary_asset_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id)'));
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get userId => i1.GeneratedColumn<String> get userId =>
columnsByName['user_id']! as i1.GeneratedColumn<String>; columnsByName['user_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get key => i1.GeneratedColumn<int> get key =>
@@ -547,20 +496,20 @@ class Shape4 extends i0.VersionedTable {
columnsByName['value']! as i1.GeneratedColumn<i2.Uint8List>; columnsByName['value']! as i1.GeneratedColumn<i2.Uint8List>;
} }
i1.GeneratedColumn<String> _column_25(String aliasedName) => i1.GeneratedColumn<String> _column_23(String aliasedName) =>
i1.GeneratedColumn<String>('user_id', aliasedName, false, i1.GeneratedColumn<String>('user_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE')); 'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<int> _column_26(String aliasedName) => i1.GeneratedColumn<int> _column_24(String aliasedName) =>
i1.GeneratedColumn<int>('key', aliasedName, false, i1.GeneratedColumn<int>('key', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<i2.Uint8List> _column_27(String aliasedName) => i1.GeneratedColumn<i2.Uint8List> _column_25(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>('value', aliasedName, false, i1.GeneratedColumn<i2.Uint8List>('value', aliasedName, false,
type: i1.DriftSqlType.blob); type: i1.DriftSqlType.blob);
class Shape5 extends i0.VersionedTable { class Shape4 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased(); Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get sharedById => i1.GeneratedColumn<String> get sharedById =>
columnsByName['shared_by_id']! as i1.GeneratedColumn<String>; columnsByName['shared_by_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sharedWithId => i1.GeneratedColumn<String> get sharedWithId =>
@@ -569,25 +518,25 @@ class Shape5 extends i0.VersionedTable {
columnsByName['in_timeline']! as i1.GeneratedColumn<bool>; columnsByName['in_timeline']! as i1.GeneratedColumn<bool>;
} }
i1.GeneratedColumn<String> _column_28(String aliasedName) => i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>('shared_by_id', aliasedName, false, i1.GeneratedColumn<String>('shared_by_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE')); 'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_29(String aliasedName) => i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>('shared_with_id', aliasedName, false, i1.GeneratedColumn<String>('shared_with_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE')); 'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<bool> _column_30(String aliasedName) => i1.GeneratedColumn<bool> _column_28(String aliasedName) =>
i1.GeneratedColumn<bool>('in_timeline', aliasedName, false, i1.GeneratedColumn<bool>('in_timeline', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("in_timeline" IN (0, 1))'), 'CHECK ("in_timeline" IN (0, 1))'),
defaultValue: const CustomExpression('0')); defaultValue: const CustomExpression('0'));
class Shape6 extends i0.VersionedTable { class Shape5 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased(); Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>; columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name => i1.GeneratedColumn<String> get name =>
@@ -602,42 +551,42 @@ class Shape6 extends i0.VersionedTable {
columnsByName['marker']! as i1.GeneratedColumn<bool>; columnsByName['marker']! as i1.GeneratedColumn<bool>;
} }
i1.GeneratedColumn<int> _column_31(String aliasedName) => i1.GeneratedColumn<int> _column_29(String aliasedName) =>
i1.GeneratedColumn<int>('backup_selection', aliasedName, false, i1.GeneratedColumn<int>('backup_selection', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<bool> _column_32(String aliasedName) => i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
i1.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false, i1.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_ios_shared_album" IN (0, 1))'), 'CHECK ("is_ios_shared_album" IN (0, 1))'),
defaultValue: const CustomExpression('0')); defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<bool> _column_33(String aliasedName) => i1.GeneratedColumn<bool> _column_31(String aliasedName) =>
i1.GeneratedColumn<bool>('marker', aliasedName, true, i1.GeneratedColumn<bool>('marker', aliasedName, true,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("marker" IN (0, 1))')); 'CHECK ("marker" IN (0, 1))'));
class Shape7 extends i0.VersionedTable { class Shape6 extends i0.VersionedTable {
Shape7({required super.source, required super.alias}) : super.aliased(); Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId => i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>; columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get albumId => i1.GeneratedColumn<String> get albumId =>
columnsByName['album_id']! as i1.GeneratedColumn<String>; columnsByName['album_id']! as i1.GeneratedColumn<String>;
} }
i1.GeneratedColumn<String> _column_34(String aliasedName) => i1.GeneratedColumn<String> _column_32(String aliasedName) =>
i1.GeneratedColumn<String>('asset_id', aliasedName, false, i1.GeneratedColumn<String>('asset_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); 'REFERENCES local_asset_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_35(String aliasedName) => i1.GeneratedColumn<String> _column_33(String aliasedName) =>
i1.GeneratedColumn<String>('album_id', aliasedName, false, i1.GeneratedColumn<String>('album_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES local_album_entity (id) ON DELETE CASCADE')); 'REFERENCES local_album_entity (id) ON DELETE CASCADE'));
class Shape8 extends i0.VersionedTable { class Shape7 extends i0.VersionedTable {
Shape8({required super.source, required super.alias}) : super.aliased(); Shape7({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId => i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>; columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get city => i1.GeneratedColumn<String> get city =>
@@ -684,71 +633,71 @@ class Shape8 extends i0.VersionedTable {
columnsByName['projection_type']! as i1.GeneratedColumn<String>; columnsByName['projection_type']! as i1.GeneratedColumn<String>;
} }
i1.GeneratedColumn<String> _column_36(String aliasedName) => i1.GeneratedColumn<String> _column_34(String aliasedName) =>
i1.GeneratedColumn<String>('asset_id', aliasedName, false, i1.GeneratedColumn<String>('asset_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_37(String aliasedName) => i1.GeneratedColumn<String> _column_35(String aliasedName) =>
i1.GeneratedColumn<String>('city', aliasedName, true, i1.GeneratedColumn<String>('city', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_38(String aliasedName) => i1.GeneratedColumn<String> _column_36(String aliasedName) =>
i1.GeneratedColumn<String>('state', aliasedName, true, i1.GeneratedColumn<String>('state', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_39(String aliasedName) => i1.GeneratedColumn<String> _column_37(String aliasedName) =>
i1.GeneratedColumn<String>('country', aliasedName, true, i1.GeneratedColumn<String>('country', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_40(String aliasedName) => i1.GeneratedColumn<DateTime> _column_38(String aliasedName) =>
i1.GeneratedColumn<DateTime>('date_time_original', aliasedName, true, i1.GeneratedColumn<DateTime>('date_time_original', aliasedName, true,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_41(String aliasedName) => i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, true, i1.GeneratedColumn<String>('description', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_42(String aliasedName) => i1.GeneratedColumn<String> _column_40(String aliasedName) =>
i1.GeneratedColumn<String>('exposure_time', aliasedName, true, i1.GeneratedColumn<String>('exposure_time', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<double> _column_43(String aliasedName) => i1.GeneratedColumn<double> _column_41(String aliasedName) =>
i1.GeneratedColumn<double>('f_number', aliasedName, true, i1.GeneratedColumn<double>('f_number', aliasedName, true,
type: i1.DriftSqlType.double); type: i1.DriftSqlType.double);
i1.GeneratedColumn<int> _column_44(String aliasedName) => i1.GeneratedColumn<int> _column_42(String aliasedName) =>
i1.GeneratedColumn<int>('file_size', aliasedName, true, i1.GeneratedColumn<int>('file_size', aliasedName, true,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<double> _column_45(String aliasedName) => i1.GeneratedColumn<double> _column_43(String aliasedName) =>
i1.GeneratedColumn<double>('focal_length', aliasedName, true, i1.GeneratedColumn<double>('focal_length', aliasedName, true,
type: i1.DriftSqlType.double); type: i1.DriftSqlType.double);
i1.GeneratedColumn<double> _column_46(String aliasedName) => i1.GeneratedColumn<double> _column_44(String aliasedName) =>
i1.GeneratedColumn<double>('latitude', aliasedName, true, i1.GeneratedColumn<double>('latitude', aliasedName, true,
type: i1.DriftSqlType.double); type: i1.DriftSqlType.double);
i1.GeneratedColumn<double> _column_47(String aliasedName) => i1.GeneratedColumn<double> _column_45(String aliasedName) =>
i1.GeneratedColumn<double>('longitude', aliasedName, true, i1.GeneratedColumn<double>('longitude', aliasedName, true,
type: i1.DriftSqlType.double); type: i1.DriftSqlType.double);
i1.GeneratedColumn<int> _column_48(String aliasedName) => i1.GeneratedColumn<int> _column_46(String aliasedName) =>
i1.GeneratedColumn<int>('iso', aliasedName, true, i1.GeneratedColumn<int>('iso', aliasedName, true,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_49(String aliasedName) => i1.GeneratedColumn<String> _column_47(String aliasedName) =>
i1.GeneratedColumn<String>('make', aliasedName, true, i1.GeneratedColumn<String>('make', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_50(String aliasedName) => i1.GeneratedColumn<String> _column_48(String aliasedName) =>
i1.GeneratedColumn<String>('model', aliasedName, true, i1.GeneratedColumn<String>('model', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_51(String aliasedName) => i1.GeneratedColumn<String> _column_49(String aliasedName) =>
i1.GeneratedColumn<String>('lens', aliasedName, true, i1.GeneratedColumn<String>('lens', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_52(String aliasedName) => i1.GeneratedColumn<String> _column_50(String aliasedName) =>
i1.GeneratedColumn<String>('orientation', aliasedName, true, i1.GeneratedColumn<String>('orientation', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_53(String aliasedName) => i1.GeneratedColumn<String> _column_51(String aliasedName) =>
i1.GeneratedColumn<String>('time_zone', aliasedName, true, i1.GeneratedColumn<String>('time_zone', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_54(String aliasedName) => i1.GeneratedColumn<int> _column_52(String aliasedName) =>
i1.GeneratedColumn<int>('rating', aliasedName, true, i1.GeneratedColumn<int>('rating', aliasedName, true,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_55(String aliasedName) => i1.GeneratedColumn<String> _column_53(String aliasedName) =>
i1.GeneratedColumn<String>('projection_type', aliasedName, true, i1.GeneratedColumn<String>('projection_type', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
class Shape9 extends i0.VersionedTable { class Shape8 extends i0.VersionedTable {
Shape9({required super.source, required super.alias}) : super.aliased(); Shape8({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>; columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name => i1.GeneratedColumn<String> get name =>
@@ -769,32 +718,32 @@ class Shape9 extends i0.VersionedTable {
columnsByName['order']! as i1.GeneratedColumn<int>; columnsByName['order']! as i1.GeneratedColumn<int>;
} }
i1.GeneratedColumn<String> _column_56(String aliasedName) => i1.GeneratedColumn<String> _column_54(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, false, i1.GeneratedColumn<String>('description', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'\'')); defaultValue: const CustomExpression('\'\''));
i1.GeneratedColumn<String> _column_57(String aliasedName) => i1.GeneratedColumn<String> _column_55(String aliasedName) =>
i1.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true, i1.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
i1.GeneratedColumn<bool> _column_58(String aliasedName) => i1.GeneratedColumn<bool> _column_56(String aliasedName) =>
i1.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false, i1.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_activity_enabled" IN (0, 1))'), 'CHECK ("is_activity_enabled" IN (0, 1))'),
defaultValue: const CustomExpression('1')); defaultValue: const CustomExpression('1'));
i1.GeneratedColumn<int> _column_59(String aliasedName) => i1.GeneratedColumn<int> _column_57(String aliasedName) =>
i1.GeneratedColumn<int>('order', aliasedName, false, i1.GeneratedColumn<int>('order', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_60(String aliasedName) => i1.GeneratedColumn<String> _column_58(String aliasedName) =>
i1.GeneratedColumn<String>('album_id', aliasedName, false, i1.GeneratedColumn<String>('album_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); 'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
class Shape10 extends i0.VersionedTable { class Shape9 extends i0.VersionedTable {
Shape10({required super.source, required super.alias}) : super.aliased(); Shape9({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get albumId => i1.GeneratedColumn<String> get albumId =>
columnsByName['album_id']! as i1.GeneratedColumn<String>; columnsByName['album_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get userId => i1.GeneratedColumn<String> get userId =>
@@ -803,12 +752,12 @@ class Shape10 extends i0.VersionedTable {
columnsByName['role']! as i1.GeneratedColumn<int>; columnsByName['role']! as i1.GeneratedColumn<int>;
} }
i1.GeneratedColumn<int> _column_61(String aliasedName) => i1.GeneratedColumn<int> _column_59(String aliasedName) =>
i1.GeneratedColumn<int>('role', aliasedName, false, i1.GeneratedColumn<int>('role', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int);
class Shape11 extends i0.VersionedTable { class Shape10 extends i0.VersionedTable {
Shape11({required super.source, required super.alias}) : super.aliased(); Shape10({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>; columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt => i1.GeneratedColumn<DateTime> get createdAt =>
@@ -835,44 +784,44 @@ class Shape11 extends i0.VersionedTable {
columnsByName['hide_at']! as i1.GeneratedColumn<DateTime>; columnsByName['hide_at']! as i1.GeneratedColumn<DateTime>;
} }
i1.GeneratedColumn<String> _column_62(String aliasedName) => i1.GeneratedColumn<String> _column_60(String aliasedName) =>
i1.GeneratedColumn<String>('data', aliasedName, false, i1.GeneratedColumn<String>('data', aliasedName, false,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_63(String aliasedName) => i1.GeneratedColumn<bool> _column_61(String aliasedName) =>
i1.GeneratedColumn<bool>('is_saved', aliasedName, false, i1.GeneratedColumn<bool>('is_saved', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_saved" IN (0, 1))'), 'CHECK ("is_saved" IN (0, 1))'),
defaultValue: const CustomExpression('0')); defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<DateTime> _column_64(String aliasedName) => i1.GeneratedColumn<DateTime> _column_62(String aliasedName) =>
i1.GeneratedColumn<DateTime>('memory_at', aliasedName, false, i1.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_65(String aliasedName) => i1.GeneratedColumn<DateTime> _column_63(String aliasedName) =>
i1.GeneratedColumn<DateTime>('seen_at', aliasedName, true, i1.GeneratedColumn<DateTime>('seen_at', aliasedName, true,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_66(String aliasedName) => i1.GeneratedColumn<DateTime> _column_64(String aliasedName) =>
i1.GeneratedColumn<DateTime>('show_at', aliasedName, true, i1.GeneratedColumn<DateTime>('show_at', aliasedName, true,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_67(String aliasedName) => i1.GeneratedColumn<DateTime> _column_65(String aliasedName) =>
i1.GeneratedColumn<DateTime>('hide_at', aliasedName, true, i1.GeneratedColumn<DateTime>('hide_at', aliasedName, true,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
class Shape12 extends i0.VersionedTable { class Shape11 extends i0.VersionedTable {
Shape12({required super.source, required super.alias}) : super.aliased(); Shape11({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId => i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>; columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get memoryId => i1.GeneratedColumn<String> get memoryId =>
columnsByName['memory_id']! as i1.GeneratedColumn<String>; columnsByName['memory_id']! as i1.GeneratedColumn<String>;
} }
i1.GeneratedColumn<String> _column_68(String aliasedName) => i1.GeneratedColumn<String> _column_66(String aliasedName) =>
i1.GeneratedColumn<String>('memory_id', aliasedName, false, i1.GeneratedColumn<String>('memory_id', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES memory_entity (id) ON DELETE CASCADE')); 'REFERENCES memory_entity (id) ON DELETE CASCADE'));
class Shape13 extends i0.VersionedTable { class Shape12 extends i0.VersionedTable {
Shape13({required super.source, required super.alias}) : super.aliased(); Shape12({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>; columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt => i1.GeneratedColumn<DateTime> get createdAt =>
@@ -881,44 +830,15 @@ class Shape13 extends i0.VersionedTable {
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>; columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId => i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>; columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name => i1.GeneratedColumn<String> get primaryAssetId =>
columnsByName['name']! as i1.GeneratedColumn<String>; columnsByName['primary_asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get faceAssetId =>
columnsByName['face_asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get thumbnailPath =>
columnsByName['thumbnail_path']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get isHidden =>
columnsByName['is_hidden']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get color =>
columnsByName['color']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get birthDate =>
columnsByName['birth_date']! as i1.GeneratedColumn<DateTime>;
} }
i1.GeneratedColumn<String> _column_69(String aliasedName) => i1.GeneratedColumn<String> _column_67(String aliasedName) =>
i1.GeneratedColumn<String>('face_asset_id', aliasedName, true, i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string,
i1.GeneratedColumn<String> _column_70(String aliasedName) =>
i1.GeneratedColumn<String>('thumbnail_path', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_71(String aliasedName) =>
i1.GeneratedColumn<bool>('is_favorite', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_favorite" IN (0, 1))')); 'REFERENCES remote_asset_entity (id)'));
i1.GeneratedColumn<bool> _column_72(String aliasedName) =>
i1.GeneratedColumn<bool>('is_hidden', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_hidden" IN (0, 1))'));
i1.GeneratedColumn<String> _column_73(String aliasedName) =>
i1.GeneratedColumn<String>('color', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_74(String aliasedName) =>
i1.GeneratedColumn<DateTime>('birth_date', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) { }) {

View File

@@ -1,36 +0,0 @@
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,
);
}
}

View File

@@ -1,13 +1,11 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.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' import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
hide ExifInfo; hide ExifInfo;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; 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.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.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:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@@ -32,66 +30,25 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
} }
Stream<RemoteAsset?> watchAsset(String id) { Stream<RemoteAsset?> watchAsset(String id) {
final stackCountRef = _db.stackEntity.id.count(); final query = _db.remoteAssetEntity
.select()
final query = _db.remoteAssetEntity.select().addColumns([ .addColumns([_db.localAssetEntity.id]).join([
_db.localAssetEntity.id,
_db.stackEntity.primaryAssetId,
stackCountRef,
]).join([
leftOuterJoin( leftOuterJoin(
_db.localAssetEntity, _db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false, 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) { return query.map((row) {
final asset = row.readTable(_db.remoteAssetEntity).toDto(); 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( return asset.copyWith(
localId: row.read(_db.localAssetEntity.id), localId: row.read(_db.localAssetEntity.id),
stackCount: stackCount,
); );
}).watchSingleOrNull(); }).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) { Future<ExifInfo?> getExif(String id) {
return _db.managers.remoteExifEntity return _db.managers.remoteExifEntity
.filter((row) => row.assetId.id.equals(id)) .filter((row) => row.assetId.id.equals(id))
@@ -189,53 +146,4 @@ 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.id.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),
);
});
});
}
} }

View File

@@ -1,87 +0,0 @@
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,
);
}

View File

@@ -57,7 +57,6 @@ class SyncApiRepository {
SyncRequestType.stacksV1, SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1, SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1,
], ],
).toJson(), ).toJson(),
); );
@@ -174,8 +173,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson, SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson, SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
SyncEntityType.personV1: SyncPersonV1.fromJson,
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
}; };
class _SyncAckV1 { class _SyncAckV1 {

View File

@@ -9,7 +9,6 @@ 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.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.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/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.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.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_album_user.entity.drift.dart';
@@ -138,7 +137,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
deletedAt: Value(asset.deletedAt), deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()), visibility: Value(asset.visibility.toAssetVisibility()),
livePhotoVideoId: Value(asset.livePhotoVideoId), livePhotoVideoId: Value(asset.livePhotoVideoId),
stackId: Value(asset.stackId),
); );
batch.insert( batch.insert(
@@ -512,48 +510,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
rethrow; 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),
thumbnailPath: Value(person.thumbnailPath),
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 { extension on AssetTypeEnum {

View File

@@ -89,8 +89,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
isFavorite: row.isFavorite, isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds, durationInSeconds: row.durationInSeconds,
livePhotoVideoId: row.livePhotoVideoId, livePhotoVideoId: row.livePhotoVideoId,
stackId: row.stackId,
stackCount: row.stackCount,
) )
: LocalAsset( : LocalAsset(
id: row.localId!, id: row.localId!,
@@ -167,25 +165,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum
.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
], ],
) )
..addColumns([_db.remoteAssetEntity.id])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query.map((row) { return query
final asset = row.readTable(_db.localAssetEntity).toDto(); .map((row) => row.readTable(_db.localAssetEntity).toDto())
return asset.copyWith( .get();
remoteId: row.read(_db.remoteAssetEntity.id),
);
}).get();
} }
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
@@ -266,50 +254,46 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required int offset, required int offset,
required int count, required int count,
}) async { }) async {
final albumData = await (_db.remoteAlbumEntity.select() return await transaction(() async {
..where((row) => row.id.equals(albumId))) final albumData = await (_db.remoteAlbumEntity.select()
.getSingleOrNull(); ..where((row) => row.id.equals(albumId)))
.getSingleOrNull();
// If album doesn't exist (was deleted), return empty list // If album doesn't exist (was deleted), return empty list
if (albumData == null) { if (albumData == null) {
return <BaseAsset>[]; return <BaseAsset>[];
} }
final isAscending = albumData.order == AlbumAssetOrder.asc; final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().join( final query = _db.remoteAssetEntity.select().join(
[ [
innerJoin( innerJoin(
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId _db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id), .equalsExp(_db.remoteAssetEntity.id),
useColumns: false, useColumns: false,
), ),
], ],
)..where( )..where(
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId), _db.remoteAlbumAssetEntity.albumId.equals(albumId),
); );
if (isAscending) { if (isAscending) {
query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
} else { } else {
query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]); query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]);
} }
query.limit(count, offset: offset); query.limit(count, offset: offset);
return query return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto()) .map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get(); .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) => TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder( _remoteQueryBuilder(
filter: (row) => filter: (row) =>
@@ -480,13 +464,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required Expression<bool> Function($RemoteAssetEntityTable row) filter, required Expression<bool> Function($RemoteAssetEntityTable row) filter,
required int offset, required int offset,
required int count, required int count,
}) { }) async {
final query = _db.remoteAssetEntity.select() return await transaction(() async {
..where(filter) final query = _db.remoteAssetEntity.select()
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) ..where(filter)
..limit(count, offset: offset); ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
});
} }
} }

View File

@@ -237,7 +237,7 @@ class SearchFilter {
String? filename; String? filename;
String? description; String? description;
String? language; String? language;
Set<PersonDto> people; Set<Person> people;
SearchLocationFilter location; SearchLocationFilter location;
SearchCameraFilter camera; SearchCameraFilter camera;
SearchDateFilter date; SearchDateFilter date;
@@ -282,7 +282,7 @@ class SearchFilter {
String? filename, String? filename,
String? description, String? description,
String? language, String? language,
Set<PersonDto>? people, Set<Person>? people,
SearchLocationFilter? location, SearchLocationFilter? location,
SearchCameraFilter? camera, SearchCameraFilter? camera,
SearchDateFilter? date, SearchDateFilter? date,

View File

@@ -57,6 +57,14 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
await ref.read(backgroundSyncProvider).cancel(); await ref.read(backgroundSyncProvider).cancel();
} }
Future.delayed(const Duration(seconds: 3), () {
context.replaceRoute(
widget.switchingToBeta
? const TabShellRoute()
: const TabControllerRoute(),
);
});
if (mounted) { if (mounted) {
setState(() { setState(() {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
@@ -96,7 +104,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
duration: Durations.long4, duration: Durations.long4,
child: hasMigrated child: hasMigrated
? Text( ? Text(
"Migration success!", "Migration success. Navigating to the new timeline...",
style: context.textTheme.titleMedium, style: context.textTheme.titleMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
) )
@@ -107,20 +115,6 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
), ),
), ),
), ),
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"),
),
),
], ],
), ),
), ),

View File

@@ -125,7 +125,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final asset = loadAsset(currentIndex.value); final asset = loadAsset(currentIndex.value);
if (asset.isRemote) { if (asset.isRemote) {
ref.read(castProvider.notifier).loadMediaOld(asset, false); ref.read(castProvider.notifier).loadMedia(asset, false);
} else { } else {
if (isCasting) { if (isCasting) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -394,7 +394,7 @@ class GalleryViewerPage extends HookConsumerWidget {
// send image to casting if the server has it // send image to casting if the server has it
if (newAsset.isRemote) { if (newAsset.isRemote) {
ref.read(castProvider.notifier).loadMediaOld(newAsset, false); ref.read(castProvider.notifier).loadMedia(newAsset, false);
} else { } else {
context.scaffoldMessenger.clearSnackBars(); context.scaffoldMessenger.clearSnackBars();

View File

@@ -143,7 +143,7 @@ class _MobileLayout extends StatelessWidget {
.toList(); .toList();
return ListView( return ListView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(top: 10.0, bottom: 56), padding: const EdgeInsets.symmetric(vertical: 10.0),
children: [ children: [
const BetaTimelineListTile(), const BetaTimelineListTile(),
...settings, ...settings,

View File

@@ -9,7 +9,6 @@ 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/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.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/routing/router.dart';
import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/migration.dart';
@@ -25,8 +24,8 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(websocketProvider.notifier).connect();
runNewSync(ref, full: true); runNewSync(ref, full: true);
}); });
} }
@@ -35,15 +34,42 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isScreenLandscape = context.orientation == Orientation.landscape; final isScreenLandscape = context.orientation == Orientation.landscape;
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,
),
),
),
),
],
);
}
final navigationDestinations = [ final navigationDestinations = [
NavigationDestination( NavigationDestination(
label: 'photos'.tr(), label: 'photos'.tr(),
icon: const Icon( icon: const Icon(
Icons.photo_library_outlined, Icons.photo_library_outlined,
), ),
selectedIcon: Icon( selectedIcon: buildIcon(
Icons.photo_library, isProcessing: false,
color: context.primaryColor, icon: Icon(
Icons.photo_library,
color: context.primaryColor,
),
), ),
), ),
NavigationDestination( NavigationDestination(
@@ -61,9 +87,12 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
icon: const Icon( icon: const Icon(
Icons.photo_album_outlined, Icons.photo_album_outlined,
), ),
selectedIcon: Icon( selectedIcon: buildIcon(
Icons.photo_album_rounded, isProcessing: false,
color: context.primaryColor, icon: Icon(
Icons.photo_album_rounded,
color: context.primaryColor,
),
), ),
), ),
NavigationDestination( NavigationDestination(
@@ -71,9 +100,12 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
icon: const Icon( icon: const Icon(
Icons.space_dashboard_outlined, Icons.space_dashboard_outlined,
), ),
selectedIcon: Icon( selectedIcon: buildIcon(
Icons.space_dashboard_rounded, isProcessing: false,
color: context.primaryColor, icon: Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
), ),
), ),
]; ];
@@ -100,7 +132,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
return AutoTabsRouter( return AutoTabsRouter(
routes: [ routes: [
const MainTimelineRoute(), const MainTimelineRoute(),
DriftSearchRoute(), SearchRoute(),
const DriftAlbumsRoute(), const DriftAlbumsRoute(),
const DriftLibraryRoute(), const DriftLibraryRoute(),
], ],
@@ -150,7 +182,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
// Album page // Album page
if (index == 2) { if (index == 2) {
ref.read(remoteAlbumProvider.notifier).refresh(); ref.read(remoteAlbumProvider.notifier).getAll();
} }
ref.read(hapticFeedbackProvider.notifier).selectionClick(); ref.read(hapticFeedbackProvider.notifier).selectionClick();

View File

@@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart';
import 'package:immich_mobile/routing/router.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_registration_form.dart';
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart'; import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@RoutePage() @RoutePage()
class PinAuthPage extends HookConsumerWidget { class PinAuthPage extends HookConsumerWidget {
@@ -20,7 +19,6 @@ class PinAuthPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final localAuthState = ref.watch(localAuthProvider); final localAuthState = ref.watch(localAuthProvider);
final showPinRegistrationForm = useState(createPinCode); final showPinRegistrationForm = useState(createPinCode);
final isBetaTimeline = Store.isBetaTimelineEnabled;
Future<void> registerBiometric(String pinCode) async { Future<void> registerBiometric(String pinCode) async {
final isRegistered = final isRegistered =
@@ -41,11 +39,7 @@ class PinAuthPage extends HookConsumerWidget {
), ),
); );
if (isBetaTimeline) { context.replaceRoute(const LockedRoute());
context.replaceRoute(const DriftLockedFolderRoute());
} else {
context.replaceRoute(const LockedRoute());
}
} }
} }
@@ -99,14 +93,8 @@ class PinAuthPage extends HookConsumerWidget {
Center( Center(
child: PinVerificationForm( child: PinVerificationForm(
autoFocus: true, autoFocus: true,
onSuccess: (_) { onSuccess: (_) =>
if (isBetaTimeline) { context.replaceRoute(const LockedRoute()),
context
.replaceRoute(const DriftLockedFolderRoute());
} else {
context.replaceRoute(const LockedRoute());
}
},
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),

View File

@@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget {
); );
showPeoplePicker() { showPeoplePicker() {
handleOnSelect(Set<PersonDto> value) { handleOnSelect(Set<Person> value) {
filter.value = filter.value.copyWith( filter.value = filter.value.copyWith(
people: value, people: value,
); );

View File

@@ -22,6 +22,16 @@ final _features = [
icon: Icons.timeline_rounded, icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), 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( _Feature(
name: 'Selection Mode Timeline', name: 'Selection Mode Timeline',
icon: Icons.developer_mode_rounded, icon: Icons.developer_mode_rounded,
@@ -112,7 +122,6 @@ final _features = [
await db.memoryEntity.deleteAll(); await db.memoryEntity.deleteAll();
await db.memoryAssetEntity.deleteAll(); await db.memoryAssetEntity.deleteAll();
await db.stackEntity.deleteAll(); await db.stackEntity.deleteAll();
await db.personEntity.deleteAll();
}, },
), ),
_Feature( _Feature(

View File

@@ -13,7 +13,7 @@ class MainTimelinePage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
return memoryLaneProvider.maybeWhen( return memoryLaneProvider.when(
data: (memories) { data: (memories) {
return memories.isEmpty return memories.isEmpty
? const Timeline(showStorageIndicator: true) ? const Timeline(showStorageIndicator: true)
@@ -26,7 +26,8 @@ class MainTimelinePage extends ConsumerWidget {
showStorageIndicator: true, showStorageIndicator: true,
); );
}, },
orElse: () => const Timeline(showStorageIndicator: true), loading: () => const Timeline(showStorageIndicator: true),
error: (error, stackTrace) => const Timeline(showStorageIndicator: true),
); );
} }
} }

View File

@@ -166,10 +166,6 @@ final _remoteStats = [
name: 'Stacks', name: 'Stacks',
load: (db) => db.managers.stackEntity.count(), load: (db) => db.managers.stackEntity.count(),
), ),
_Stat(
name: 'People',
load: (db) => db.managers.personEntity.count(),
),
]; ];
@RoutePage() @RoutePage()

View File

@@ -40,7 +40,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
// Load albums when component mounts // Load albums when component mounts
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(remoteAlbumProvider.notifier).refresh(); ref.read(remoteAlbumProvider.notifier).getAll();
}); });
searchController.addListener(() { searchController.addListener(() {
@@ -88,20 +88,17 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final albums = final albumState = ref.watch(remoteAlbumProvider);
ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums)); final albums = albumState.filteredAlbums;
final isLoading = albumState.isLoading;
final error = albumState.error;
final userId = ref.watch(currentUserProvider)?.id; final userId = ref.watch(currentUserProvider)?.id;
return RefreshIndicator( return RefreshIndicator(
onRefresh: onRefresh, onRefresh: onRefresh,
edgeOffset: 100,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
ImmichSliverAppBar( ImmichSliverAppBar(
snap: false,
floating: false,
pinned: true,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon( icon: const Icon(
@@ -136,10 +133,14 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
? _AlbumGrid( ? _AlbumGrid(
albums: albums, albums: albums,
userId: userId, userId: userId,
isLoading: isLoading,
error: error,
) )
: _AlbumList( : _AlbumList(
albums: albums, albums: albums,
userId: userId, userId: userId,
isLoading: isLoading,
error: error,
), ),
], ],
), ),
@@ -480,15 +481,46 @@ class _QuickSortAndViewMode extends StatelessWidget {
class _AlbumList extends ConsumerWidget { class _AlbumList extends ConsumerWidget {
const _AlbumList({ const _AlbumList({
required this.isLoading,
required this.error,
required this.albums, required this.albums,
required this.userId, required this.userId,
}); });
final bool isLoading;
final String? error;
final List<RemoteAlbum> albums; final List<RemoteAlbum> albums;
final String? userId; final String? userId;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
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) { if (albums.isEmpty) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: Center( child: Center(
@@ -591,13 +623,44 @@ class _AlbumGrid extends StatelessWidget {
const _AlbumGrid({ const _AlbumGrid({
required this.albums, required this.albums,
required this.userId, required this.userId,
required this.isLoading,
required this.error,
}); });
final List<RemoteAlbum> albums; final List<RemoteAlbum> albums;
final String? userId; final String? userId;
final bool isLoading;
final String? error;
@override @override
Widget build(BuildContext context) { 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) { if (albums.isEmpty) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: Center( child: Center(

View File

@@ -4,45 +4,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage() @RoutePage()
class DriftLockedFolderPage extends ConsumerStatefulWidget { class DriftLockedFolderPage extends StatelessWidget {
const DriftLockedFolderPage({super.key}); const DriftLockedFolderPage({super.key});
@override
ConsumerState<DriftLockedFolderPage> createState() =>
_DriftLockedFolderPageState();
}
class _DriftLockedFolderPageState extends ConsumerState<DriftLockedFolderPage>
with WidgetsBindingObserver {
bool _showOverlay = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (mounted) {
setState(() {
_showOverlay = state != AppLifecycleState.resumed;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ProviderScope( return ProviderScope(
@@ -61,18 +30,12 @@ class _DriftLockedFolderPageState extends ConsumerState<DriftLockedFolderPage>
}, },
), ),
], ],
child: _showOverlay child: Timeline(
? const SizedBox() appBar: MesmerizingSliverAppBar(
: PopScope( title: 'locked_folder'.t(context: context),
onPopInvokedWithResult: (didPop, _) => ),
didPop ? ref.read(authProvider.notifier).lockPinCode() : null, bottomSheet: const LockedFolderBottomSheet(),
child: Timeline( ),
appBar: MesmerizingSliverAppBar(
title: 'locked_folder'.t(context: context),
),
bottomSheet: const LockedFolderBottomSheet(),
),
),
); );
} }
} }

View File

@@ -1,11 +1,9 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage() @RoutePage()
class DriftRecentlyTakenPage extends StatelessWidget { class DriftRecentlyTakenPage extends StatelessWidget {
@@ -31,9 +29,7 @@ class DriftRecentlyTakenPage extends StatelessWidget {
}, },
), ),
], ],
child: Timeline( child: const Timeline(),
appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t()),
),
); );
} }
} }

View File

@@ -1,11 +1,9 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage() @RoutePage()
class DriftVideoPage extends StatelessWidget { class DriftVideoPage extends StatelessWidget {
@@ -29,9 +27,7 @@ class DriftVideoPage extends StatelessWidget {
}, },
), ),
], ],
child: Timeline( child: const Timeline(),
appBar: MesmerizingSliverAppBar(title: 'videos'.t()),
),
); );
} }
} }

View File

@@ -30,7 +30,6 @@ class LocalTimelinePage extends StatelessWidget {
child: Timeline( child: Timeline(
appBar: MesmerizingSliverAppBar(title: album.name), appBar: MesmerizingSliverAppBar(title: album.name),
bottomSheet: const LocalAlbumBottomSheet(), bottomSheet: const LocalAlbumBottomSheet(),
showStorageIndicator: true,
), ),
); );
} }

View File

@@ -1,925 +0,0 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
@RoutePage()
class DriftSearchPage extends HookConsumerWidget {
const DriftSearchPage({super.key, this.preFilter});
final SearchFilter? preFilter;
@override
Widget build(BuildContext context, WidgetRef ref) {
final textSearchType = useState<TextSearchType>(TextSearchType.context);
final searchHintText =
useState<String>('sunrise_on_the_beach'.t(context: context));
final textSearchController = useTextEditingController();
final filter = useState<SearchFilter>(
SearchFilter(
people: preFilter?.people ?? {},
location: preFilter?.location ?? SearchLocationFilter(),
camera: preFilter?.camera ?? SearchCameraFilter(),
date: preFilter?.date ?? SearchDateFilter(),
display: preFilter?.display ??
SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
mediaType: preFilter?.mediaType ?? AssetType.other,
language:
"${context.locale.languageCode}-${context.locale.countryCode}",
),
);
final previousFilter = useState<SearchFilter?>(null);
final peopleCurrentFilterWidget = useState<Widget?>(null);
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
final cameraCurrentFilterWidget = useState<Widget?>(null);
final locationCurrentFilterWidget = useState<Widget?>(null);
final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
final displayOptionCurrentFilterWidget = useState<Widget?>(null);
final isSearching = useState(false);
SnackBar searchInfoSnackBar(String message) {
return SnackBar(
content: Text(
message,
style: context.textTheme.labelLarge,
),
showCloseIcon: true,
behavior: SnackBarBehavior.fixed,
closeIconColor: context.colorScheme.onSurface,
);
}
search() async {
if (filter.value.isEmpty) {
return;
}
if (preFilter == null && filter.value == previousFilter.value) {
return;
}
isSearching.value = true;
ref.watch(paginatedSearchProvider.notifier).clear();
final hasResult = await ref
.watch(paginatedSearchProvider.notifier)
.search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_result'.t(context: context)),
);
}
previousFilter.value = filter.value;
isSearching.value = false;
}
loadMoreSearchResult() async {
isSearching.value = true;
final hasResult = await ref
.watch(paginatedSearchProvider.notifier)
.search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_more_result'.t(context: context)),
);
}
isSearching.value = false;
}
searchPreFilter() {
if (preFilter != null) {
Future.delayed(
Duration.zero,
() {
search();
if (preFilter!.location.city != null) {
locationCurrentFilterWidget.value = Text(
preFilter!.location.city!,
style: context.textTheme.labelLarge,
);
}
},
);
}
}
useEffect(
() {
Future.microtask(
() => ref.invalidate(paginatedSearchProvider),
);
searchPreFilter();
return null;
},
[],
);
showPeoplePicker() {
handleOnSelect(Set<PersonDto> value) {
filter.value = filter.value.copyWith(
people: value,
);
peopleCurrentFilterWidget.value = Text(
value
.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context))
.join(', '),
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
people: {},
);
peopleCurrentFilterWidget.value = null;
search();
}
showFilterBottomSheet(
context: context,
isScrollControlled: true,
child: FractionallySizedBox(
heightFactor: 0.8,
child: FilterBottomSheetScaffold(
title: 'search_filter_people_title'.t(context: context),
expanded: true,
onSearch: search,
onClear: handleClear,
child: PeoplePicker(
onSelect: handleOnSelect,
filter: filter.value.people,
),
),
),
);
}
showLocationPicker() {
handleOnSelect(Map<String, String?> value) {
filter.value = filter.value.copyWith(
location: SearchLocationFilter(
country: value['country'],
city: value['city'],
state: value['state'],
),
);
final locationText = <String>[];
if (value['country'] != null) {
locationText.add(value['country']!);
}
if (value['state'] != null) {
locationText.add(value['state']!);
}
if (value['city'] != null) {
locationText.add(value['city']!);
}
locationCurrentFilterWidget.value = Text(
locationText.join(', '),
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
location: SearchLocationFilter(),
);
locationCurrentFilterWidget.value = null;
search();
}
showFilterBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
child: FilterBottomSheetScaffold(
title: 'search_filter_location_title'.t(context: context),
onSearch: search,
onClear: handleClear,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LocationPicker(
onSelected: handleOnSelect,
filter: filter.value.location,
),
),
),
),
),
);
}
showCameraPicker() {
handleOnSelect(Map<String, String?> value) {
filter.value = filter.value.copyWith(
camera: SearchCameraFilter(
make: value['make'],
model: value['model'],
),
);
cameraCurrentFilterWidget.value = Text(
'${value['make'] ?? ''} ${value['model'] ?? ''}',
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
camera: SearchCameraFilter(),
);
cameraCurrentFilterWidget.value = null;
search();
}
showFilterBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
child: FilterBottomSheetScaffold(
title: 'search_filter_camera_title'.t(context: context),
onSearch: search,
onClear: handleClear,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CameraPicker(
onSelect: handleOnSelect,
filter: filter.value.camera,
),
),
),
);
}
showDatePicker() async {
final firstDate = DateTime(1900);
final lastDate = DateTime.now();
final date = await showDateRangePicker(
context: context,
firstDate: firstDate,
lastDate: lastDate,
currentDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: filter.value.date.takenAfter ?? lastDate,
end: filter.value.date.takenBefore ?? lastDate,
),
helpText: 'search_filter_date_title'.t(context: context),
cancelText: 'cancel'.t(context: context),
confirmText: 'select'.t(context: context),
saveText: 'save'.t(context: context),
errorFormatText: 'invalid_date_format'.t(context: context),
errorInvalidText: 'invalid_date'.t(context: context),
fieldStartHintText: 'start_date'.t(context: context),
fieldEndHintText: 'end_date'.t(context: context),
initialEntryMode: DatePickerEntryMode.calendar,
keyboardType: TextInputType.text,
);
if (date == null) {
filter.value = filter.value.copyWith(
date: SearchDateFilter(),
);
dateRangeCurrentFilterWidget.value = null;
search();
return;
}
filter.value = filter.value.copyWith(
date: SearchDateFilter(
takenAfter: date.start,
takenBefore: date.end.add(
const Duration(
hours: 23,
minutes: 59,
seconds: 59,
),
),
),
);
// If date range is less than 24 hours, set the end date to the end of the day
if (date.end.difference(date.start).inHours < 24) {
dateRangeCurrentFilterWidget.value = Text(
DateFormat.yMMMd().format(date.start.toLocal()),
style: context.textTheme.labelLarge,
);
} else {
dateRangeCurrentFilterWidget.value = Text(
'search_filter_date_interval'.t(
context: context,
args: {
"start": DateFormat.yMMMd().format(date.start.toLocal()),
"end": DateFormat.yMMMd().format(date.end.toLocal()),
},
),
style: context.textTheme.labelLarge,
);
}
search();
}
// MEDIA PICKER
showMediaTypePicker() {
handleOnSelected(AssetType assetType) {
filter.value = filter.value.copyWith(
mediaType: assetType,
);
mediaTypeCurrentFilterWidget.value = Text(
assetType == AssetType.image
? 'image'.t(context: context)
: assetType == AssetType.video
? 'video'.t(context: context)
: 'all'.t(context: context),
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
mediaType: AssetType.other,
);
mediaTypeCurrentFilterWidget.value = null;
search();
}
showFilterBottomSheet(
context: context,
child: FilterBottomSheetScaffold(
title: 'search_filter_media_type_title'.t(context: context),
onSearch: search,
onClear: handleClear,
child: MediaTypePicker(
onSelect: handleOnSelected,
filter: filter.value.mediaType,
),
),
);
}
// DISPLAY OPTION
showDisplayOptionPicker() {
handleOnSelect(Map<DisplayOption, bool> value) {
final filterText = <String>[];
value.forEach((key, value) {
switch (key) {
case DisplayOption.notInAlbum:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isNotInAlbum: value,
),
);
if (value) {
filterText.add(
'search_filter_display_option_not_in_album'
.t(context: context),
);
}
break;
case DisplayOption.archive:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isArchive: value,
),
);
if (value) {
filterText.add('archive'.t(context: context));
}
break;
case DisplayOption.favorite:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isFavorite: value,
),
);
if (value) {
filterText.add('favorite'.t(context: context));
}
break;
}
});
if (filterText.isEmpty) {
displayOptionCurrentFilterWidget.value = null;
return;
}
displayOptionCurrentFilterWidget.value = Text(
filterText.join(', '),
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
);
displayOptionCurrentFilterWidget.value = null;
search();
}
showFilterBottomSheet(
context: context,
child: FilterBottomSheetScaffold(
title: 'display_options'.t(context: context),
onSearch: search,
onClear: handleClear,
child: DisplayOptionPicker(
onSelect: handleOnSelect,
filter: filter.value.display,
),
),
);
}
handleTextSubmitted(String value) {
switch (textSearchType.value) {
case TextSearchType.context:
filter.value = filter.value.copyWith(
filename: '',
context: value,
description: '',
);
break;
case TextSearchType.filename:
filter.value = filter.value.copyWith(
filename: value,
context: '',
description: '',
);
break;
case TextSearchType.description:
filter.value = filter.value.copyWith(
filename: '',
context: '',
description: value,
);
break;
}
search();
}
IconData getSearchPrefixIcon() => switch (textSearchType.value) {
TextSearchType.context => Icons.image_search_rounded,
TextSearchType.filename => Icons.abc_rounded,
TextSearchType.description => Icons.text_snippet_outlined,
};
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: MenuAnchor(
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
),
),
builder: (
BuildContext context,
MenuController controller,
Widget? child,
) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert_rounded),
tooltip: 'Show text search menu',
);
},
menuChildren: [
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.image_search_rounded),
title: Text(
'search_by_context'.t(context: context),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.context
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.context,
),
onPressed: () {
textSearchType.value = TextSearchType.context;
searchHintText.value =
'sunrise_on_the_beach'.t(context: context);
},
),
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.abc_rounded),
title: Text(
'search_filter_filename'.t(context: context),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.filename
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.filename,
),
onPressed: () {
textSearchType.value = TextSearchType.filename;
searchHintText.value =
'file_name_or_extension'.t(context: context);
},
),
MenuItemButton(
child: ListTile(
leading: const Icon(Icons.text_snippet_outlined),
title: Text(
'search_by_description'.t(context: context),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color:
textSearchType.value == TextSearchType.description
? context.colorScheme.primary
: null,
),
),
selectedColor: context.colorScheme.primary,
selected:
textSearchType.value == TextSearchType.description,
),
onPressed: () {
textSearchType.value = TextSearchType.description;
searchHintText.value =
'search_by_description_example'.t(context: context);
},
),
],
),
),
],
title: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(0),
width: 0,
),
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withValues(alpha: 0.075),
context.colorScheme.primary.withValues(alpha: 0.09),
context.colorScheme.primary.withValues(alpha: 0.075),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SearchField(
hintText: searchHintText.value,
key: const Key('search_text_field'),
controller: textSearchController,
contentPadding: preFilter != null
? const EdgeInsets.only(left: 24)
: const EdgeInsets.all(8),
prefixIcon: preFilter != null
? null
: Icon(
getSearchPrefixIcon(),
color: context.colorScheme.primary,
),
onSubmitted: handleTextSubmitted,
focusNode: ref.watch(searchInputFocusProvider),
),
),
),
body: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(top: 12.0),
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView(
key: const Key('search_filter_chip_list'),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
SearchFilterChip(
icon: Icons.people_alt_outlined,
onTap: showPeoplePicker,
label: 'people'.t(context: context),
currentFilter: peopleCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.location_on_outlined,
onTap: showLocationPicker,
label: 'search_filter_location'.t(context: context),
currentFilter: locationCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.camera_alt_outlined,
onTap: showCameraPicker,
label: 'camera'.t(context: context),
currentFilter: cameraCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.date_range_outlined,
onTap: showDatePicker,
label: 'search_filter_date'.t(context: context),
currentFilter: dateRangeCurrentFilterWidget.value,
),
SearchFilterChip(
key: const Key('media_type_chip'),
icon: Icons.video_collection_outlined,
onTap: showMediaTypePicker,
label: 'search_filter_media_type'.t(context: context),
currentFilter: mediaTypeCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.display_settings_outlined,
onTap: showDisplayOptionPicker,
label:
'search_filter_display_options'.t(context: context),
currentFilter: displayOptionCurrentFilterWidget.value,
),
],
),
),
),
),
if (isSearching.value)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
)
else
_SearchResultGrid(onScrollEnd: loadMoreSearchResult),
],
),
);
}
}
class _SearchResultGrid extends ConsumerWidget {
final VoidCallback onScrollEnd;
const _SearchResultGrid({required this.onScrollEnd});
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchResult = ref.watch(paginatedSearchProvider);
if (searchResult.totalAssets == 0) {
return const _SearchEmptyContent();
}
return NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
final isBottomSheetNotification = notification.context
?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() !=
null;
final metrics = notification.metrics;
final isVerticalScroll = metrics.axis == Axis.vertical;
if (metrics.pixels >= metrics.maxScrollExtent &&
isVerticalScroll &&
!isBottomSheetNotification) {
onScrollEnd();
}
return true;
},
child: SliverFillRemaining(
child: ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref
.watch(timelineFactoryProvider)
.fromAssets(searchResult.assets);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: Timeline(
key: ValueKey(searchResult.totalAssets),
appBar: null,
groupBy: GroupAssetsBy.none,
),
),
),
);
}
}
class _SearchEmptyContent extends StatelessWidget {
const _SearchEmptyContent();
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: ListView(
shrinkWrap: true,
children: [
const SizedBox(height: 40),
Center(
child: Image.asset(
context.isDarkTheme
? 'assets/polaroid-dark.png'
: 'assets/polaroid-light.png',
height: 125,
),
),
const SizedBox(height: 16),
Center(
child: Text(
'search_page_search_photos_videos'.t(context: context),
style: context.textTheme.labelLarge,
),
),
const SizedBox(height: 32),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: _QuickLinkList(),
),
],
),
);
}
}
class _QuickLinkList extends StatelessWidget {
const _QuickLinkList();
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
border: Border.all(
color: context.colorScheme.outline.withAlpha(10),
width: 1,
),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
context.colorScheme.primary.withAlpha(15),
context.colorScheme.primary.withAlpha(20),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_QuickLink(
title: 'recently_taken'.t(context: context),
icon: Icons.schedule_outlined,
isTop: true,
onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()),
),
_QuickLink(
title: 'videos'.t(context: context),
icon: Icons.play_circle_outline_rounded,
onTap: () => context.pushRoute(const DriftVideoRoute()),
),
_QuickLink(
title: 'favorites'.t(context: context),
icon: Icons.favorite_border_rounded,
isBottom: true,
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
),
],
),
);
}
}
class _QuickLink extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onTap;
final bool isTop;
final bool isBottom;
const _QuickLink({
required this.title,
required this.icon,
required this.onTap,
this.isTop = false,
this.isBottom = false,
});
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.only(
topLeft: Radius.circular(isTop ? 20 : 0),
topRight: Radius.circular(isTop ? 20 : 0),
bottomLeft: Radius.circular(isBottom ? 20 : 0),
bottomRight: Radius.circular(isBottom ? 20 : 0),
);
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
leading: Icon(
icon,
size: 26,
),
title: Text(
title,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
onTap: onTap,
);
}
}

View File

@@ -1,40 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/search_result.model.dart';
import 'package:immich_mobile/domain/services/search.service.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
final paginatedSearchProvider =
StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
);
class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
final SearchService _searchService;
PaginatedSearchNotifier(this._searchService)
: super(const SearchResult(assets: [], nextPage: 1));
Future<bool> search(SearchFilter filter) async {
if (state.nextPage == null) {
return false;
}
final result = await _searchService.search(filter, state.nextPage!);
if (result == null) {
return false;
}
state = SearchResult(
assets: [...state.assets, ...result.assets],
nextPage: result.nextPage,
);
return true;
}
clear() {
state = const SearchResult(assets: [], nextPage: 1);
}
}

View File

@@ -6,7 +6,6 @@ class BaseActionButton extends StatelessWidget {
super.key, super.key,
required this.label, required this.label,
required this.iconData, required this.iconData,
this.iconColor,
this.onPressed, this.onPressed,
this.onLongPressed, this.onLongPressed,
this.maxWidth = 90.0, this.maxWidth = 90.0,
@@ -16,7 +15,6 @@ class BaseActionButton extends StatelessWidget {
final String label; final String label;
final IconData iconData; final IconData iconData;
final Color? iconColor;
final double maxWidth; final double maxWidth;
final double? minWidth; final double? minWidth;
final bool menuItem; final bool menuItem;
@@ -29,8 +27,7 @@ class BaseActionButton extends StatelessWidget {
minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
final iconTheme = IconTheme.of(context); final iconTheme = IconTheme.of(context);
final iconSize = iconTheme.size ?? 24.0; final iconSize = iconTheme.size ?? 24.0;
final iconColor = final iconColor = iconTheme.color ?? context.themeData.iconTheme.color;
this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
final textColor = context.themeData.textTheme.labelLarge?.color; final textColor = context.themeData.textTheme.labelLarge?.color;
if (menuItem) { if (menuItem) {

View File

@@ -1,32 +0,0 @@
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/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
class CastActionButton extends ConsumerWidget {
const CastActionButton({super.key, this.menuItem = true});
final bool menuItem;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
return BaseActionButton(
iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
iconColor:
isCasting ? context.primaryColor : null, // null = default color
label: "cast".t(context: context),
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
},
menuItem: menuItem,
);
}
}

View File

@@ -1,49 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class ShareActionButton extends ConsumerWidget { class ShareActionButton extends ConsumerWidget {
final ActionSource source; const ShareActionButton({super.key});
const ShareActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).shareAssets(source);
ref.read(multiSelectProvider.notifier).reset();
if (!context.mounted) {
return;
}
if (!result.success) {
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
} else if (result.count > 0) {
ImmichToast.show(
context: context,
msg: 'share_action_prompt'
.t(context: context, args: {'count': result.count.toString()}),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.success,
);
}
}
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -51,7 +14,6 @@ class ShareActionButton extends ConsumerWidget {
iconData: iconData:
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
label: 'share'.t(context: context), label: 'share'.t(context: context),
onPressed: () => _onTap(context, ref),
); );
} }
} }

View File

@@ -1,56 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class StackActionButton extends ConsumerWidget { class StackActionButton extends ConsumerWidget {
final ActionSource source; const StackActionButton({super.key});
const StackActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access stack action');
}
final result =
await ref.read(actionProvider.notifier).stack(user.id, source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'stack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton( return BaseActionButton(
iconData: Icons.filter_none_rounded, iconData: Icons.filter_none_rounded,
label: "stack".t(context: context), label: "stack".t(context: context),
onPressed: () => _onTap(context, ref),
); );
} }
} }

View File

@@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class UnStackActionButton extends ConsumerWidget {
final ActionSource source;
const UnStackActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result = await ref.read(actionProvider.notifier).unStack(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unstack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.filter_none_rounded,
label: "unstack".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
class StackChildrenNotifier
extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset?> {
@override
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
if (asset == null ||
asset is! RemoteAsset ||
asset.stackId == null ||
// The stackCount check is to ensure we only fetch stacks for timelines that have stacks
asset.stackCount == 0) {
return const [];
}
return ref.watch(assetServiceProvider).getStack(asset);
}
}
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(
StackChildrenNotifier.new,
);

View File

@@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
class AssetStackRow extends ConsumerWidget {
const AssetStackRow({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
final showControls =
ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
opacity = 0;
}
final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset));
return IgnorePointer(
ignoring: opacity < 255,
child: AnimatedOpacity(
opacity: opacity / 255,
duration: Durations.short2,
child: ref.watch(stackChildrenNotifier(asset)).when(
data: (state) => SizedBox.square(
dimension: 80,
child: _StackList(stack: state),
),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
),
);
}
}
class _StackList extends ConsumerWidget {
final List<RemoteAsset> stack;
const _StackList({required this.stack});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(
left: 5,
right: 5,
bottom: 30,
),
itemCount: stack.length,
itemBuilder: (ctx, index) {
final asset = stack[index];
return Padding(
padding: const EdgeInsets.only(right: 5),
child: GestureDetector(
onTap: () {
ref.read(assetViewerProvider.notifier).setStackIndex(index);
ref.read(currentAssetNotifier.notifier).setAsset(asset);
},
child: Container(
height: 60,
width: 60,
decoration: index ==
ref.watch(assetViewerProvider.select((s) => s.stackIndex))
? const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(color: Colors.white, width: 2),
),
)
: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
border: null,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Stack(
fit: StackFit.expand,
children: [
Image(
fit: BoxFit.cover,
image: getThumbnailImageProvider(
remoteId: asset.id,
size: const Size.square(60),
),
),
if (asset.isVideo)
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
),
],
),
),
),
),
);
},
);
}
}

View File

@@ -1,17 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
@@ -22,10 +18,8 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
@@ -89,7 +83,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
double previousExtent = _kBottomSheetMinimumExtent; double previousExtent = _kBottomSheetMinimumExtent;
Offset dragDownPosition = Offset.zero; Offset dragDownPosition = Offset.zero;
int totalAssets = 0; int totalAssets = 0;
int stackIndex = 0;
BuildContext? scaffoldContext; BuildContext? scaffoldContext;
Map<String, GlobalKey> videoPlayerKeys = {}; Map<String, GlobalKey> videoPlayerKeys = {};
@@ -172,10 +165,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _onAssetChanged(int index) { void _onAssetChanged(int index) {
final asset = ref.read(timelineServiceProvider).getAsset(index); final asset = ref.read(timelineServiceProvider).getAsset(index);
// Always holds the current asset from the timeline
ref.read(assetViewerProvider.notifier).setAsset(asset);
// The currentAssetNotifier actually holds the current asset that is displayed
// which could be stack children as well
ref.read(currentAssetNotifier.notifier).setAsset(asset); ref.read(currentAssetNotifier.notifier).setAsset(asset);
if (asset.isVideo || asset.isMotionPhoto) { if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset(); ref.read(videoPlaybackValueProvider.notifier).reset();
@@ -195,40 +184,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
}); });
_delayedOperations.add(timer); _delayedOperations.add(timer);
_handleCasting(asset);
}
void _handleCasting(BaseAsset asset) {
if (!ref.read(castProvider).isCasting) return;
// hide any casting snackbars if they exist
context.scaffoldMessenger.hideCurrentSnackBar();
// send image to casting if the server has it
if (asset.hasRemote) {
final remoteAsset = asset as RemoteAsset;
ref.read(castProvider.notifier).loadMedia(remoteAsset, false);
} else {
// casting cannot show local assets
context.scaffoldMessenger.clearSnackBars();
if (ref.read(castProvider).isCasting) {
ref.read(castProvider.notifier).stop();
context.scaffoldMessenger.showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
content: Text(
"local_asset_cast_failed".tr(),
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
),
),
);
}
}
} }
void _onPageBuild(PhotoViewControllerBase controller) { void _onPageBuild(PhotoViewControllerBase controller) {
@@ -497,12 +452,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
ImageChunkEvent? progress, ImageChunkEvent? progress,
int index, int index,
) { ) {
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); final asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) {
asset = stackChildren
.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
}
return Container( return Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
@@ -530,14 +480,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) { PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
scaffoldContext ??= ctx; scaffoldContext ??= ctx;
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); final asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) {
asset = stackChildren
.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
}
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
if (asset.isImage && !isPlayingMotionVideo) { if (asset.isImage && !isPlayingMotionVideo) {
return _imageBuilder(ctx, asset); return _imageBuilder(ctx, asset);
} }
@@ -623,24 +568,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
// Using multiple selectors to avoid unnecessary rebuilds for other state changes // Using multiple selectors to avoid unnecessary rebuilds for other state changes
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
ref.watch(isPlayingMotionVideoProvider); ref.watch(isPlayingMotionVideoProvider);
// Listen for casting changes and send initial asset to the cast provider
ref.listen(castProvider.select((value) => value.isCasting),
(_, isCasting) async {
if (!isCasting) return;
final asset = ref.read(currentAssetNotifier);
if (asset == null) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleCasting(asset);
});
});
final isInLockedView = ref.watch(inLockedViewProvider);
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way. // Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
// Issue: https://github.com/flutter/flutter/issues/109037 // Issue: https://github.com/flutter/flutter/issues/109037
// TODO: Add a custom scrum builder once the fix lands on stable // TODO: Add a custom scrum builder once the fix lands on stable
@@ -667,17 +596,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
backgroundDecoration: BoxDecoration(color: backgroundColor), backgroundDecoration: BoxDecoration(color: backgroundColor),
enablePanAlways: true, enablePanAlways: true,
), ),
bottomNavigationBar: showingBottomSheet bottomNavigationBar: const ViewerBottomBar(),
? const SizedBox.shrink()
: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AssetStackRow(),
if (!isInLockedView) const ViewerBottomBar(),
],
),
), ),
); );
} }

View File

@@ -1,40 +1,26 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
class ViewerOpenBottomSheetEvent extends Event {
const ViewerOpenBottomSheetEvent();
}
class AssetViewerState { class AssetViewerState {
final int backgroundOpacity; final int backgroundOpacity;
final bool showingBottomSheet; final bool showingBottomSheet;
final bool showingControls; final bool showingControls;
final BaseAsset? currentAsset;
final int stackIndex;
const AssetViewerState({ const AssetViewerState({
this.backgroundOpacity = 255, this.backgroundOpacity = 255,
this.showingBottomSheet = false, this.showingBottomSheet = false,
this.showingControls = true, this.showingControls = true,
this.currentAsset,
this.stackIndex = 0,
}); });
AssetViewerState copyWith({ AssetViewerState copyWith({
int? backgroundOpacity, int? backgroundOpacity,
bool? showingBottomSheet, bool? showingBottomSheet,
bool? showingControls, bool? showingControls,
BaseAsset? currentAsset,
int? stackIndex,
}) { }) {
return AssetViewerState( return AssetViewerState(
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet, showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet,
showingControls: showingControls ?? this.showingControls, showingControls: showingControls ?? this.showingControls,
currentAsset: currentAsset ?? this.currentAsset,
stackIndex: stackIndex ?? this.stackIndex,
); );
} }
@@ -50,18 +36,14 @@ class AssetViewerState {
return other is AssetViewerState && return other is AssetViewerState &&
other.backgroundOpacity == backgroundOpacity && other.backgroundOpacity == backgroundOpacity &&
other.showingBottomSheet == showingBottomSheet && other.showingBottomSheet == showingBottomSheet &&
other.showingControls == showingControls && other.showingControls == showingControls;
other.currentAsset == currentAsset &&
other.stackIndex == stackIndex;
} }
@override @override
int get hashCode => int get hashCode =>
backgroundOpacity.hashCode ^ backgroundOpacity.hashCode ^
showingBottomSheet.hashCode ^ showingBottomSheet.hashCode ^
showingControls.hashCode ^ showingControls.hashCode;
currentAsset.hashCode ^
stackIndex.hashCode;
} }
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> { class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
@@ -70,10 +52,6 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
return const AssetViewerState(); return const AssetViewerState();
} }
void setAsset(BaseAsset? asset) {
state = state.copyWith(currentAsset: asset, stackIndex: 0);
}
void setOpacity(int opacity) { void setOpacity(int opacity) {
state = state.copyWith( state = state.copyWith(
backgroundOpacity: opacity, backgroundOpacity: opacity,
@@ -98,10 +76,6 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
void toggleControls() { void toggleControls() {
state = state.copyWith(showingControls: !state.showingControls); state = state.copyWith(showingControls: !state.showingControls);
} }
void setStackIndex(int index) {
state = state.copyWith(stackIndex: index);
}
} }
final assetViewerProvider = final assetViewerProvider =

View File

@@ -3,7 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
@@ -36,7 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
} }
final actions = <Widget>[ final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(),
const _EditActionButton(),
if (asset.hasRemote && isOwner) if (asset.hasRemote && isOwner)
const ArchiveActionButton(source: ActionSource.viewer), const ArchiveActionButton(source: ActionSource.viewer),
]; ];
@@ -83,3 +86,15 @@ class ViewerBottomBar extends ConsumerWidget {
); );
} }
} }
class _EditActionButton extends ConsumerWidget {
const _EditActionButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.tune_outlined,
label: 'edit'.t(context: context),
);
}
}

View File

@@ -18,7 +18,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
@@ -45,10 +44,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
serverInfoProvider.select((state) => state.serverFeatures.trash), serverInfoProvider.select((state) => state.serverFeatures.trash),
); );
final isInLockedView = ref.watch(inLockedViewProvider);
final actions = <Widget>[ final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(),
if (asset.hasRemote) ...[ if (asset.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.viewer), const ShareLinkActionButton(source: ActionSource.viewer),
const ArchiveActionButton(source: ActionSource.viewer), const ArchiveActionButton(source: ActionSource.viewer),
@@ -66,10 +63,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
], ],
]; ];
final lockedViewActions = <Widget>[];
return BaseBottomSheet( return BaseBottomSheet(
actions: isInLockedView ? lockedViewActions : actions, actions: actions,
slivers: const [_AssetDetailBottomSheet()], slivers: const [_AssetDetailBottomSheet()],
controller: controller, controller: controller,
initialChildSize: initialChildSize, initialChildSize: initialChildSize,
@@ -78,7 +73,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
expand: false, expand: false,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
resizeOnScroll: false, resizeOnScroll: false,
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
); );
} }
} }
@@ -90,18 +84,14 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
final dateTime = asset.createdAt.toLocal(); final dateTime = asset.createdAt.toLocal();
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
final timezone = dateTime.timeZoneOffset.isNegative return '$date$_kSeparator$time';
? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'
: 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}';
return '$date$_kSeparator$time $timezone';
} }
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
final height = asset.height ?? exifInfo?.height; final height = asset.height ?? exifInfo?.height;
final width = asset.width ?? exifInfo?.width; final width = asset.width ?? exifInfo?.width;
final resolution = (width != null && height != null) final resolution =
? "${width.toInt()} x ${height.toInt()}" (width != null && height != null) ? "$width x $height" : null;
: null;
final fileSize = final fileSize =
exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null; exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
@@ -160,46 +150,46 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
// Asset Date and Time // Asset Date and Time
_SheetTile( _SheetTile(
title: _getDateTime(context, asset), title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith( titleStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w500,
fontSize: 16,
), ),
), ),
const SheetLocationDetails(), const SheetLocationDetails(),
// Details header // Details header
_SheetTile( _SheetTile(
title: 'exif_bottom_sheet_details'.t(context: context), title: 'exif_bottom_sheet_details'.t(context: context),
titleStyle: context.textTheme.labelMedium?.copyWith( titleStyle: context.textTheme.labelLarge,
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
), ),
// File info // File info
_SheetTile( _SheetTile(
title: asset.name, title: asset.name,
titleStyle: context.textTheme.labelLarge, titleStyle: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.w600),
leading: Icon( leading: Icon(
asset.isImage ? Icons.image_outlined : Icons.videocam_outlined, asset.isImage ? Icons.image_outlined : Icons.videocam_outlined,
size: 24, size: 30,
color: context.textTheme.labelLarge?.color, color: context.textTheme.labelLarge?.color,
), ),
subtitle: _getFileInfo(asset, exifInfo), subtitle: _getFileInfo(asset, exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith( subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.bodyMedium?.color?.withAlpha(155), color: context.textTheme.labelLarge?.color?.withAlpha(200),
), ),
), ),
// Camera info // Camera info
if (cameraTitle != null) if (cameraTitle != null)
_SheetTile( _SheetTile(
title: cameraTitle, title: cameraTitle,
titleStyle: context.textTheme.labelLarge, titleStyle: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.w600),
leading: Icon( leading: Icon(
Icons.camera_outlined, Icons.camera_outlined,
size: 24, size: 30,
color: context.textTheme.labelLarge?.color, color: context.textTheme.labelLarge?.color,
), ),
subtitle: _getCameraInfoSubtitle(exifInfo), subtitle: _getCameraInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith( subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.bodyMedium?.color?.withAlpha(155), color: context.textTheme.labelLarge?.color?.withAlpha(200),
), ),
), ),
], ],

View File

@@ -72,7 +72,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
// Guard no lat/lng // Guard no lat/lng
if (!hasCoordinates || if (!hasCoordinates ||
(asset != null && asset is LocalAsset && asset!.hasRemote)) { (asset is LocalAsset && !(asset as LocalAsset).hasRemote)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -95,10 +95,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Text( child: Text(
"exif_bottom_sheet_location".t(context: context), "exif_bottom_sheet_location".t(context: context),
style: context.textTheme.labelMedium?.copyWith( style: context.textTheme.labelLarge,
color: context.textTheme.labelMedium?.color?.withAlpha(200),
fontWeight: FontWeight.w600,
),
), ),
), ),
ExifMap( ExifMap(
@@ -112,13 +109,15 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
padding: const EdgeInsets.only(bottom: 4.0), padding: const EdgeInsets.only(bottom: 4.0),
child: Text( child: Text(
locationName, locationName,
style: context.textTheme.labelLarge, style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
), ),
), ),
Text( Text(
coordinates, coordinates,
style: context.textTheme.labelMedium?.copyWith( style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150), color: context.textTheme.labelLarge?.color?.withAlpha(150),
), ),
), ),
], ],

View File

@@ -5,16 +5,12 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ViewerTopAppBar({super.key}); const ViewerTopAppBar({super.key});
@@ -28,7 +24,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isInLockedView = ref.watch(inLockedViewProvider);
final isShowingSheet = ref final isShowingSheet = ref
.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); .watch(assetViewerProvider.select((state) => state.showingBottomSheet));
@@ -42,17 +37,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
opacity = 0; opacity = 0;
} }
final isCasting = ref.watch(
castProvider.select((c) => c.isCasting),
);
final websocketConnected =
ref.watch(websocketProvider.select((c) => c.isConnected));
final actions = <Widget>[ final actions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
if (asset.hasRemote && isOwner && !asset.isFavorite) if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.hasRemote && isOwner && asset.isFavorite) if (asset.hasRemote && isOwner && asset.isFavorite)
@@ -64,14 +49,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
const _KebabMenu(), const _KebabMenu(),
]; ];
final lockedViewActions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
const _KebabMenu(),
];
return IgnorePointer( return IgnorePointer(
ignoring: opacity < 255, ignoring: opacity < 255,
child: AnimatedOpacity( child: AnimatedOpacity(
@@ -84,11 +61,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
iconTheme: const IconThemeData(size: 22, color: Colors.white), iconTheme: const IconThemeData(size: 22, color: Colors.white),
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
shape: const Border(), shape: const Border(),
actions: isShowingSheet actions: isShowingSheet ? null : actions,
? null
: isInLockedView
? lockedViewActions
: actions,
), ),
), ),
); );

View File

@@ -33,7 +33,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
maxChildSize: 0.4, maxChildSize: 0.4,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
const ShareActionButton(source: ActionSource.timeline), const ShareActionButton(),
if (multiselect.hasRemote) ...[ if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline), const ShareLinkActionButton(source: ActionSource.timeline),
const UnArchiveActionButton(source: ActionSource.timeline), const UnArchiveActionButton(source: ActionSource.timeline),
@@ -49,7 +49,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const MoveToLockFolderActionButton( const MoveToLockFolderActionButton(
source: ActionSource.timeline, source: ActionSource.timeline,
), ),
const StackActionButton(source: ActionSource.timeline), const StackActionButton(),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -14,7 +14,6 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
final bool expand; final bool expand;
final bool shouldCloseOnMinExtent; final bool shouldCloseOnMinExtent;
final bool resizeOnScroll; final bool resizeOnScroll;
final Color? backgroundColor;
const BaseBottomSheet({ const BaseBottomSheet({
super.key, super.key,
@@ -27,7 +26,6 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
this.expand = true, this.expand = true,
this.shouldCloseOnMinExtent = true, this.shouldCloseOnMinExtent = true,
this.resizeOnScroll = true, this.resizeOnScroll = true,
this.backgroundColor,
}); });
@override @override
@@ -71,8 +69,8 @@ class _BaseDraggableScrollableSheetState
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
builder: (BuildContext context, ScrollController scrollController) { builder: (BuildContext context, ScrollController scrollController) {
return Card( return Card(
color: widget.backgroundColor ?? color: context.colorScheme.surfaceContainerHigh,
context.colorScheme.surfaceContainerHigh, surfaceTintColor: context.colorScheme.surfaceContainerHigh,
borderOnForeground: false, borderOnForeground: false,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
elevation: 6.0, elevation: 6.0,

View File

@@ -33,7 +33,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
maxChildSize: 0.4, maxChildSize: 0.4,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
const ShareActionButton(source: ActionSource.timeline), const ShareActionButton(),
if (multiselect.hasRemote) ...[ if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline), const ShareLinkActionButton(source: ActionSource.timeline),
const UnFavoriteActionButton(source: ActionSource.timeline), const UnFavoriteActionButton(source: ActionSource.timeline),
@@ -49,7 +49,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const MoveToLockFolderActionButton( const MoveToLockFolderActionButton(
source: ActionSource.timeline, source: ActionSource.timeline,
), ),
const StackActionButton(source: ActionSource.timeline), const StackActionButton(),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -33,7 +33,7 @@ class GeneralBottomSheet extends ConsumerWidget {
maxChildSize: 0.4, maxChildSize: 0.4,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
const ShareActionButton(source: ActionSource.timeline), const ShareActionButton(),
if (multiselect.hasRemote) ...[ if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline), const ShareLinkActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline),
@@ -49,7 +49,7 @@ class GeneralBottomSheet extends ConsumerWidget {
const MoveToLockFolderActionButton( const MoveToLockFolderActionButton(
source: ActionSource.timeline, source: ActionSource.timeline,
), ),
const StackActionButton(source: ActionSource.timeline), const StackActionButton(),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -16,7 +16,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget {
maxChildSize: 0.4, maxChildSize: 0.4,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
ShareActionButton(source: ActionSource.timeline), ShareActionButton(),
DeleteLocalActionButton(source: ActionSource.timeline), DeleteLocalActionButton(source: ActionSource.timeline),
UploadActionButton(), UploadActionButton(),
], ],

View File

@@ -17,7 +17,7 @@ class LockedFolderBottomSheet extends ConsumerWidget {
maxChildSize: 0.4, maxChildSize: 0.4,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
ShareActionButton(source: ActionSource.timeline), ShareActionButton(),
DownloadActionButton(), DownloadActionButton(),
DeletePermanentActionButton(source: ActionSource.timeline), DeletePermanentActionButton(source: ActionSource.timeline),
RemoveFromLockFolderActionButton(source: ActionSource.timeline), RemoveFromLockFolderActionButton(source: ActionSource.timeline),

Some files were not shown because too many files have changed in this diff Show More