diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml
index 62a97a01eb..0543f42317 100644
--- a/.devcontainer/mobile/container-compose-overrides.yml
+++ b/.devcontainer/mobile/container-compose-overrides.yml
@@ -11,8 +11,8 @@ services:
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- - ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
- - ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
+ - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
+ - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
database:
diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml
index d7efc92cb1..24ac9734b1 100644
--- a/.devcontainer/server/container-compose-overrides.yml
+++ b/.devcontainer/server/container-compose-overrides.yml
@@ -13,8 +13,8 @@ services:
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
- - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
+ - ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload
+ - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload
- /etc/localtime:/etc/localtime:ro
immich-web:
diff --git a/.dockerignore b/.dockerignore
index d152800d1b..f7efb5c56e 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,41 +1,41 @@
.vscode/
.github/
.git/
+.env*
+*.log
+*.tmp
+*.temp
+
+**/Dockerfile
+**/node_modules/
+**/.pnpm-store/
+**/dist/
+**/coverage/
+**/build/
design/
docker/
-Dockerfile
!docker/scripts
+
docs/
!docs/package.json
!docs/package-lock.json
+
e2e/
!e2e/package.json
!e2e/package-lock.json
+
fastlane/
machine-learning/
misc/
mobile/
-cli/coverage/
-cli/dist/
-cli/node_modules/
-cli/Dockerfile
-
open-api/typescript-sdk/build/
-open-api/typescript-sdk/node_modules/
+!open-api/typescript-sdk/package.json
+!open-api/typescript-sdk/package-lock.json
-server/coverage/
-server/node_modules/
server/upload/
server/src/queries
-server/dist/
server/www/
-server/Dockerfile
-web/node_modules/
-web/coverage/
web/.svelte-kit
-web/build/
-web/.env
-web/Dockerfile
diff --git a/.gitattributes b/.gitattributes
index 3d43ff20ed..e3fb061bbc 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -12,6 +12,12 @@ mobile/lib/**/*.drift.dart linguist-generated=true
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
+mobile/lib/infrastructure/repositories/db.repository.steps.dart -diff -merge
+mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generated=true
+
+mobile/test/drift/main/generated/** -diff -merge
+mobile/test/drift/main/generated/** linguist-generated=true
+
open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 0344189043..4cb788f5fa 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -61,8 +61,7 @@ jobs:
run: dart pub get
- name: Install DCM
- # TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
- uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
+ uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
version: auto
diff --git a/.gitignore b/.gitignore
index b4ebd04841..af85d96c02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml
vite.config.js.timestamp-*
+.pnpm-store
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 56d1817317..32ff115102 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -16,7 +16,7 @@ name: immich-dev
services:
immich-server:
container_name: immich_server
- command: ['/usr/src/app/bin/immich-dev']
+ command: ['immich-dev']
image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
@@ -27,11 +27,11 @@ services:
target: dev
restart: unless-stopped
volumes:
- - ../server:/usr/src/app
- - ../open-api:/usr/src/open-api
+ - ../server:/usr/src/app/server
+ - ../open-api:/usr/src/app/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- - /usr/src/app/node_modules
+ - /usr/src/app/server/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
@@ -69,19 +69,20 @@ services:
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
# user: 0:0
build:
- context: ../web
- command: ['/usr/src/app/bin/immich-web']
+ context: ../
+ dockerfile: web/Dockerfile
+ command: ['immich-web']
env_file:
- .env
ports:
- 3000:3000
- 24678:24678
volumes:
- - ../web:/usr/src/app
- - ../i18n:/usr/src/i18n
- - ../open-api/:/usr/src/open-api/
+ - ../web:/usr/src/app/web
+ - ../i18n:/usr/src/app/i18n
+ - ../open-api/:/usr/src/app/open-api/
# - ../../ui:/usr/ui
- - /usr/src/app/node_modules
+ - /usr/src/app/web/node_modules
ulimits:
nofile:
soft: 1048576
diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md
index b414f5deaa..b275d8fede 100644
--- a/docs/docs/administration/server-commands.md
+++ b/docs/docs/administration/server-commands.md
@@ -2,16 +2,17 @@
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
-| Command | Description |
-| ------------------------ | ------------------------------------- |
-| `help` | Display help |
-| `reset-admin-password` | Reset the password for the admin user |
-| `disable-password-login` | Disable password login |
-| `enable-password-login` | Enable password login |
-| `enable-oauth-login` | Enable OAuth login |
-| `disable-oauth-login` | Disable OAuth login |
-| `list-users` | List Immich users |
-| `version` | Print Immich version |
+| Command | Description |
+| ------------------------ | ------------------------------------------------------------- |
+| `help` | Display help |
+| `reset-admin-password` | Reset the password for the admin user |
+| `disable-password-login` | Disable password login |
+| `enable-password-login` | Enable password login |
+| `enable-oauth-login` | Enable OAuth login |
+| `disable-oauth-login` | Disable OAuth login |
+| `list-users` | List Immich users |
+| `version` | Print Immich version |
+| `change-media-location` | Change database file paths to align with a new media location |
## How to run a command
@@ -88,3 +89,24 @@ Print Immich Version
immich-admin version
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)
+```
diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md
index 1aab5ad327..c4c5396466 100644
--- a/docs/docs/developer/devcontainers.md
+++ b/docs/docs/developer/devcontainers.md
@@ -7,7 +7,7 @@ sidebar_position: 3
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
-[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
+Get started fast!
[](https://codespaces.new/immich-app/immich/)
@@ -71,7 +71,7 @@ cd immich
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
-:::important Required Configuration
+:::important Configuration
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
```bash
@@ -88,6 +88,10 @@ source ~/.bashrc
### Step 3: Launch the Dev Container
+:::tip
+Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command.
+:::
+
#### Using VS Code UI:
1. Open the cloned repository in VS Code
diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md
index b9dab5b9c8..8d5ab55049 100644
--- a/docs/docs/install/environment-variables.md
+++ b/docs/docs/install/environment-variables.md
@@ -29,20 +29,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## General
-| Variable | Description | Default | Containers | Workers |
-| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
-| `TZ` | Timezone | \*1 | server | 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_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `./upload`\*3 | 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 | |
-| `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_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_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 |
+| Variable | Description | Default | Containers | Workers |
+| :---------------------------------- | :---------------------------------------------------------------------------------------- | :---------------------------------: | :----------------------- | :----------------- |
+| `TZ` | Timezone | \*1 | server | 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_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/usr/src/app/upload`\*3 | 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 | |
+| `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_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_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 |
\*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.
diff --git a/docs/static/_redirects b/docs/static/_redirects
index 6683c78077..7b01d1e3bb 100644
--- a/docs/static/_redirects
+++ b/docs/static/_redirects
@@ -1,4 +1,5 @@
-/docs /docs/overview/introduction 307
+/docs /docs/overview/welcome 307
+/docs/ /docs/overview/welcome 307
/docs/mobile-app-beta-program /docs/features/mobile-app 307
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
/docs/install /docs/install/docker-compose 307
@@ -30,4 +31,4 @@
/docs/guides/api-album-sync /docs/community-projects 307
/docs/guides/remove-offline-files /docs/community-projects 307
/milestones /roadmap 307
-/docs/overview/introduction /docs/overview/welcome 307
\ No newline at end of file
+/docs/overview/introduction /docs/overview/welcome 307
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index 6ec5402360..26c3951278 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -3,7 +3,6 @@ name: immich-e2e
services:
immich-server:
container_name: immich-e2e-server
- command: ['./start.sh']
image: immich-server:latest
build:
context: ../
diff --git a/i18n/en.json b/i18n/en.json
index 7f7dae833d..bda2fee4fb 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -373,10 +373,12 @@
"admin_password": "Admin Password",
"administration": "Administration",
"advanced": "Advanced",
+ "advanced_settings_beta_timeline_subtitle": "Try the new app experience",
+ "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_title": "[EXPERIMENTAL] Use alternate device album sync filter",
"advanced_settings_log_level_title": "Log level: {level}",
- "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
+ "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from local assets. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
@@ -404,6 +406,7 @@
"album_options": "Album options",
"album_remove_user": "Remove user?",
"album_remove_user_confirmation": "Are you sure you want to remove {user}?",
+ "album_search_not_found": "No albums found matching your search",
"album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.",
"album_updated": "Album updated",
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
@@ -423,6 +426,7 @@
"albums_default_sort_order": "Default album sort order",
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
"albums_feature_description": "Collections of assets that can be shared with other users.",
+ "albums_on_device_count": "Albums on device ({count})",
"all": "All",
"all_albums": "All albums",
"all_people": "All people",
@@ -603,6 +607,7 @@
"cancel": "Cancel",
"cancel_search": "Cancel search",
"canceled": "Canceled",
+ "canceling": "Canceling",
"cannot_merge_people": "Cannot merge people",
"cannot_undo_this_action": "You cannot undo this action!",
"cannot_update_the_description": "Cannot update the description",
@@ -749,6 +754,7 @@
"delete_key": "Delete key",
"delete_library": "Delete Library",
"delete_link": "Delete link",
+ "delete_local_action_prompt": "{count} deleted locally",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_others": "Delete others",
@@ -762,6 +768,7 @@
"description": "Description",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
+ "deselect_all": "Deselect All",
"details": "Details",
"direction": "Direction",
"disabled": "Disabled",
@@ -779,6 +786,7 @@
"documentation": "Documentation",
"done": "Done",
"download": "Download",
+ "download_action_prompt": "Downloading {count} assets",
"download_canceled": "Download canceled",
"download_complete": "Download complete",
"download_enqueue": "Download enqueued",
@@ -835,6 +843,7 @@
"empty_trash": "Empty trash",
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
"enable": "Enable",
+ "enable_backup": "Enable Backup",
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
"enabled": "Enabled",
"end_date": "End date",
@@ -1146,6 +1155,7 @@
"library_page_sort_created": "Created date",
"library_page_sort_last_modified": "Last modified",
"library_page_sort_title": "Album title",
+ "licenses": "Licenses",
"light": "Light",
"like_deleted": "Like deleted",
"link_motion_video": "Link motion video",
@@ -1480,6 +1490,7 @@
"purchase_server_description_2": "Supporter status",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "The server product key is managed by the admin",
+ "queue_status": "Queuing {count}/{total}",
"rating": "Star rating",
"rating_clear": "Clear rating",
"rating_count": "{count, plural, one {# star} other {# stars}}",
@@ -1690,6 +1701,7 @@
"settings_saved": "Settings saved",
"setup_pin_code": "Setup a PIN code",
"share": "Share",
+ "share_action_prompt": "Shared {count} assets",
"share_add_photos": "Add photos",
"share_assets_selected": "{count} selected",
"share_dialog_preparing": "Preparing...",
@@ -1791,6 +1803,7 @@
"sort_title": "Title",
"source": "Source",
"stack": "Stack",
+ "stack_action_prompt": "{count} stacked",
"stack_duplicates": "Stack duplicates",
"stack_select_one_photo": "Select one main photo for the stack",
"stack_selected_photos": "Stack selected photos",
@@ -1901,6 +1914,7 @@
"unselect_all_duplicates": "Unselect all duplicates",
"unselect_all_in": "Unselect all in {group}",
"unstack": "Un-stack",
+ "unstack_action_prompt": "{count} unstacked",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untagged": "Untagged",
"up_next": "Up next",
@@ -1908,6 +1922,7 @@
"updated_password": "Updated password",
"upload": "Upload",
"upload_concurrency": "Upload concurrency",
+ "upload_details": "Upload Details",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_title": "Upload Asset",
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
@@ -1939,6 +1954,7 @@
"user_usage_stats_description": "View account usage statistics",
"username": "Username",
"users": "Users",
+ "users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
"utilities": "Utilities",
"validate": "Validate",
"validate_endpoint_error": "Please enter a valid URL",
@@ -1957,6 +1973,7 @@
"view_album": "View Album",
"view_all": "View All",
"view_all_users": "View all users",
+ "view_details": "View Details",
"view_in_timeline": "View in timeline",
"view_link": "View link",
"view_links": "View links",
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index 2c52595a26..7a03b54a95 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -106,6 +106,7 @@ custom_lint:
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
+ - lib/domain/services/search.service.dart
# refactor
- lib/models/map/map_marker.model.dart
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 870e424461..8b4dc42b7e 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -3,6 +3,8 @@ plugins {
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id 'com.google.devtools.ksp'
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version
+
}
def localProperties = new Properties()
@@ -45,6 +47,10 @@ android {
main.java.srcDirs += 'src/main/kotlin'
}
+ buildFeatures {
+ compose true
+ }
+
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
@@ -105,6 +111,8 @@ dependencies {
def guava_version = '33.3.1-android'
def glide_version = '4.16.0'
def serialization_version = '1.8.1'
+ def compose_version = '1.1.1'
+ def gson_version = '2.10.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
@@ -116,6 +124,17 @@ dependencies {
ksp "com.github.bumptech.glide:ksp:$glide_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
+
+ //Glance Widget
+ implementation "androidx.glance:glance-appwidget:$compose_version"
+ implementation "com.google.code.gson:gson:$gson_version"
+
+ // Glance Configure
+ implementation "androidx.activity:activity-compose:1.8.2"
+ implementation "androidx.compose.ui:ui:$compose_version"
+ implementation "androidx.compose.ui:ui-tooling:$compose_version"
+ implementation "androidx.compose.material3:material3:1.2.1"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}
// This is uncommented in F-Droid build script
diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro
index ea6dd795b5..898caee06c 100644
--- a/mobile/android/app/proguard-rules.pro
+++ b/mobile/android/app/proguard-rules.pro
@@ -25,8 +25,15 @@
@com.google.gson.annotations.SerializedName ;
}
+# TypeToken preventions
+-keep class com.google.gson.reflect.TypeToken { *; }
+-keep class * extends com.google.gson.reflect.TypeToken
+
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
-##---------------End: proguard configuration for Gson ----------
\ No newline at end of file
+##---------------End: proguard configuration for Gson ----------
+
+# Keep all widget model classes and their fields for Gson
+-keep class app.alextran.immich.widget.model.** { *; }
\ No newline at end of file
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index cf3b7ee719..09276f6d4a 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -141,6 +141,41 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -154,4 +189,4 @@
-
\ No newline at end of file
+
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt
index 0fb75b002c..9c90528dc9 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt
@@ -83,6 +83,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart()
+
}
return resolvableFuture
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt
new file mode 100644
index 0000000000..9188df1700
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt
@@ -0,0 +1,33 @@
+package app.alextran.immich.widget
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import java.io.File
+
+fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? {
+ val options = BitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ BitmapFactory.decodeFile(file.absolutePath, options)
+
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
+ options.inJustDecodeBounds = false
+
+ return BitmapFactory.decodeFile(file.absolutePath, options)
+}
+
+fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
+ val (height: Int, width: Int) = options.run { outHeight to outWidth }
+ var inSampleSize = 1
+
+ if (height > reqHeight || width > reqWidth) {
+ val halfHeight: Int = height / 2
+ val halfWidth: Int = width / 2
+
+ while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
+ inSampleSize *= 2
+ }
+ }
+
+ return inSampleSize
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt
new file mode 100644
index 0000000000..3915f291f8
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt
@@ -0,0 +1,244 @@
+package app.alextran.immich.widget
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.datastore.preferences.core.Preferences
+import androidx.glance.*
+import androidx.glance.appwidget.GlanceAppWidgetManager
+import androidx.glance.appwidget.state.updateAppWidgetState
+import androidx.work.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import androidx.glance.appwidget.state.getAppWidgetState
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import app.alextran.immich.widget.model.*
+import java.time.LocalDate
+
+class ImageDownloadWorker(
+ private val context: Context,
+ workerParameters: WorkerParameters
+) : CoroutineWorker(context, workerParameters) {
+
+ companion object {
+
+ private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
+
+ private fun buildConstraints(): Constraints {
+ return Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ }
+
+ private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
+ return Data.Builder()
+ .putString(kWorkerWidgetType, widgetType.toString())
+ .putInt(kWorkerWidgetID, appWidgetId)
+ .build()
+ }
+
+ fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
+ val manager = WorkManager.getInstance(context)
+
+ val workRequest = PeriodicWorkRequestBuilder(
+ 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()
+ .setConstraints(buildConstraints())
+ .setInputData(buildInputData(appWidgetId, widgetType))
+ .addTag(appWidgetId.toString())
+ .build()
+
+ manager.enqueueUniqueWork(
+ "$uniqueWorkName-$appWidgetId",
+ ExistingWorkPolicy.REPLACE,
+ workRequest
+ )
+ }
+
+ suspend fun cancel(context: Context, appWidgetId: Int) {
+ WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId")
+
+ // delete cached image
+ val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
+ val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
+ val currentImgUUID = widgetConfig[kImageUUID]
+
+ if (!currentImgUUID.isNullOrEmpty()) {
+ val file = File(context.cacheDir, imageFilename(currentImgUUID))
+ file.delete()
+ }
+ }
+ }
+
+ override suspend fun doWork(): Result {
+ return try {
+ val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
+ val widgetId = inputData.getInt(kWorkerWidgetID, -1)
+ val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
+ val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
+ val currentImgUUID = widgetConfig[kImageUUID]
+
+ val serverConfig = ImmichAPI.getServerConfig(context)
+
+ // clear any image caches and go to "login" state if no credentials
+ if (serverConfig == null) {
+ if (!currentImgUUID.isNullOrEmpty()) {
+ deleteImage(currentImgUUID)
+ updateWidget(
+ glanceId,
+ "",
+ "",
+ "immich://",
+ WidgetState.LOG_IN
+ )
+ }
+
+ return Result.success()
+ }
+
+ // fetch new image
+ val entry = when (widgetType) {
+ WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
+ WidgetType.MEMORIES -> fetchMemory(serverConfig)
+ }
+
+ // clear current image if it exists
+ if (!currentImgUUID.isNullOrEmpty()) {
+ deleteImage(currentImgUUID)
+ }
+
+ // save a new image
+ val imgUUID = UUID.randomUUID().toString()
+ saveImage(entry.image, imgUUID)
+
+ // trigger the update routine with new image uuid
+ updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
+
+ Result.success()
+ } catch (e: Exception) {
+ Log.e(uniqueWorkName, "Error while loading image", e)
+ if (runAttemptCount < 10) {
+ Result.retry()
+ } else {
+ Result.failure()
+ }
+ }
+ }
+
+ private suspend fun updateWidget(
+ glanceId: GlanceId,
+ imageUUID: String,
+ subtitle: String?,
+ deeplink: String?,
+ widgetState: WidgetState = WidgetState.SUCCESS
+ ) {
+ updateAppWidgetState(context, glanceId) { prefs ->
+ prefs[kNow] = System.currentTimeMillis()
+ prefs[kImageUUID] = imageUUID
+ prefs[kWidgetState] = widgetState.toString()
+ prefs[kSubtitleText] = subtitle ?: ""
+ prefs[kDeeplinkURL] = deeplink ?: ""
+ }
+
+ PhotoWidget().update(context,glanceId)
+ }
+
+ private suspend fun fetchRandom(
+ serverConfig: ServerConfig,
+ widgetConfig: Preferences
+ ): WidgetEntry {
+ val api = ImmichAPI(serverConfig)
+
+ val filters = SearchFilters()
+ val albumId = widgetConfig[kSelectedAlbum]
+ val showSubtitle = widgetConfig[kShowAlbumName]
+ val albumName = widgetConfig[kSelectedAlbumName]
+ var subtitle: String? = if (showSubtitle == true) albumName else ""
+
+
+ if (albumId == "FAVORITES") {
+ filters.isFavorite = true
+ } else if (albumId != null) {
+ filters.albumIds = listOf(albumId)
+ }
+
+ var randomSearch = api.fetchSearchResults(filters)
+
+ // handle an empty album, fallback to random
+ if (randomSearch.isEmpty() && albumId != null) {
+ randomSearch = api.fetchSearchResults(SearchFilters())
+ subtitle = ""
+ }
+
+ val random = randomSearch.first()
+ val image = api.fetchImage(random)
+
+ return WidgetEntry(
+ image,
+ subtitle,
+ assetDeeplink(random)
+ )
+ }
+
+ private suspend fun fetchMemory(
+ serverConfig: ServerConfig
+ ): WidgetEntry {
+ val api = ImmichAPI(serverConfig)
+
+ val today = LocalDate.now()
+ val memories = api.fetchMemory(today)
+ val asset: Asset
+ var subtitle: String? = null
+
+ if (memories.isNotEmpty()) {
+ // pick a random asset from a random memory
+ val memory = memories.random()
+ asset = memory.assets.random()
+
+ val yearDiff = today.year - memory.data.year
+ subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago"
+ } else {
+ val filters = SearchFilters(size=1)
+ asset = api.fetchSearchResults(filters).first()
+ }
+
+ val image = api.fetchImage(asset)
+ return WidgetEntry(
+ image,
+ subtitle,
+ assetDeeplink(asset)
+ )
+ }
+
+ private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
+ val file = File(context.cacheDir, imageFilename(uuid))
+ file.delete()
+ }
+
+ private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
+ val file = File(context.cacheDir, imageFilename(uuid))
+ FileOutputStream(file).use { out ->
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
+ }
+ }
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt
new file mode 100644
index 0000000000..42f5fb4b1b
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt
@@ -0,0 +1,103 @@
+package app.alextran.immich.widget
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import app.alextran.immich.widget.model.*
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import es.antonborri.home_widget.HomeWidgetPlugin
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.OutputStreamWriter
+import java.net.HttpURLConnection
+import java.net.URL
+import java.net.URLEncoder
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+class ImmichAPI(cfg: ServerConfig) {
+
+ companion object {
+ fun getServerConfig(context: Context): ServerConfig? {
+ val prefs = HomeWidgetPlugin.getData(context)
+
+ val serverURL = prefs.getString("widget_server_url", "") ?: ""
+ val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
+
+ if (serverURL.isBlank() || sessionKey.isBlank()) {
+ return null
+ }
+
+ return ServerConfig(
+ serverURL,
+ sessionKey
+ )
+ }
+ }
+
+
+ private val gson = Gson()
+ private val serverConfig = cfg
+
+ private fun buildRequestURL(endpoint: String, params: List> = 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 = 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>() {}.type
+ gson.fromJson(response, type)
+ }
+
+ suspend fun fetchMemory(date: LocalDate): List = 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>() {}.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 = 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>() {}.type
+ gson.fromJson(response, type)
+ }
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt
new file mode 100644
index 0000000000..7721af7d6f
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt
@@ -0,0 +1,56 @@
+package app.alextran.immich.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import app.alextran.immich.widget.model.*
+import es.antonborri.home_widget.HomeWidgetPlugin
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MemoryReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget = PhotoWidget()
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+
+ appWidgetIds.forEach { widgetID ->
+ ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES)
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
+
+ // Launch coroutine to setup a single shot if the app requested the update
+ if (fromMainApp) {
+ CoroutineScope(Dispatchers.Default).launch {
+ val provider = ComponentName(context, MemoryReceiver::class.java)
+ val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
+
+ glanceIds.forEach { widgetID ->
+ ImageDownloadWorker.singleShot(context, widgetID, WidgetType.MEMORIES)
+ }
+ }
+ }
+
+ super.onReceive(context, intent)
+ }
+
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
+ super.onDeleted(context, appWidgetIds)
+ CoroutineScope(Dispatchers.Default).launch {
+ appWidgetIds.forEach { id ->
+ ImageDownloadWorker.cancel(context, id)
+ }
+ }
+ }
+}
+
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt
new file mode 100644
index 0000000000..b1a0a9de31
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt
@@ -0,0 +1,124 @@
+package app.alextran.immich.widget
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.*
+import androidx.core.net.toUri
+import androidx.datastore.preferences.core.MutablePreferences
+import androidx.glance.appwidget.*
+import androidx.glance.*
+import androidx.glance.action.clickable
+import androidx.glance.layout.*
+import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import androidx.glance.text.Text
+import androidx.glance.text.TextAlign
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+import app.alextran.immich.R
+import app.alextran.immich.widget.model.*
+import java.io.File
+
+class PhotoWidget : GlanceAppWidget() {
+ override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ provideContent {
+ val prefs = currentState()
+
+ 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))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt
new file mode 100644
index 0000000000..39afd76c35
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt
@@ -0,0 +1,55 @@
+package app.alextran.immich.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import es.antonborri.home_widget.HomeWidgetPlugin
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import app.alextran.immich.widget.model.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class RandomReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget = PhotoWidget()
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+
+ appWidgetIds.forEach { widgetID ->
+ ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM)
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
+
+ // Launch coroutine to setup a single shot if the app requested the update
+ if (fromMainApp) {
+ CoroutineScope(Dispatchers.Default).launch {
+ val provider = ComponentName(context, RandomReceiver::class.java)
+ val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
+
+ glanceIds.forEach { widgetID ->
+ ImageDownloadWorker.singleShot(context, widgetID, WidgetType.RANDOM)
+ }
+ }
+ }
+
+ super.onReceive(context, intent)
+ }
+
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
+ super.onDeleted(context, appWidgetIds)
+ CoroutineScope(Dispatchers.Default).launch {
+ appWidgetIds.forEach { id ->
+ ImageDownloadWorker.cancel(context, id)
+ }
+ }
+ }
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt
new file mode 100644
index 0000000000..74686ee0b8
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/Dropdown.kt
@@ -0,0 +1,64 @@
+package app.alextran.immich.widget.configure
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.*
+
+
+data class DropdownItem (
+ val label: String,
+ val id: String,
+)
+
+// Creating a composable to display a drop down menu
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun Dropdown(items: List,
+ 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
+ )
+ }
+ }
+ }
+ }
+
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt
new file mode 100644
index 0000000000..efdcc41540
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/LightDarkTheme.kt
@@ -0,0 +1,28 @@
+package app.alextran.immich.widget.configure
+
+import android.os.Build
+import androidx.compose.foundation.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun LightDarkTheme(
+ content: @Composable () -> Unit
+) {
+ val context = LocalContext.current
+ val isDarkTheme = isSystemInDarkTheme()
+
+ val colorScheme = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme ->
+ dynamicDarkColorScheme(context)
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme ->
+ dynamicLightColorScheme(context)
+ isDarkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content
+ )
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt
new file mode 100644
index 0000000000..83e404a8f1
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt
@@ -0,0 +1,210 @@
+package app.alextran.immich.widget.configure
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.appwidget.GlanceAppWidgetManager
+import androidx.glance.appwidget.state.getAppWidgetState
+import androidx.glance.appwidget.state.updateAppWidgetState
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import app.alextran.immich.widget.ImageDownloadWorker
+import app.alextran.immich.widget.ImmichAPI
+import app.alextran.immich.widget.model.*
+import kotlinx.coroutines.launch
+import java.io.FileNotFoundException
+
+class RandomConfigure : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Get widget ID from intent
+ val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID)
+ ?: AppWidgetManager.INVALID_APPWIDGET_ID
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish()
+ return
+ }
+
+ val glanceId = GlanceAppWidgetManager(applicationContext)
+ .getGlanceIdBy(appWidgetId)
+
+ setContent {
+ LightDarkTheme {
+ RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = {
+ finish()
+ Log.w("WIDGET_ACTIVITY", "SAVING")
+ })
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) {
+
+ var selectedAlbum by remember { mutableStateOf(null) }
+ var showAlbumName by remember { mutableStateOf(false) }
+ var availableAlbums by remember { mutableStateOf>(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
+
+ try {
+ albumItems = api.fetchAlbums().map {
+ DropdownItem(it.albumName, it.id)
+ }
+
+ state = WidgetConfigState.SUCCESS
+ } catch (e: FileNotFoundException) {
+ Log.e("WidgetWorker", "Error fetching albums: ${e.message}")
+
+ state = WidgetConfigState.NO_CONNECTION
+ albumItems = listOf(DropdownItem(currentAlbumName, currentAlbumId))
+ }
+
+ availableAlbums = listOf(DropdownItem("None", "NONE"), DropdownItem("Favorites", "FAVORITES")) + albumItems
+
+ // load selected configuration
+ val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId }
+ selectedAlbum = albumEntity ?: availableAlbums.first()
+
+ // load showAlbumName
+ showAlbumName = currentState[kShowAlbumName] == true
+ }
+
+ suspend fun saveConfiguration() {
+ updateAppWidgetState(context, glanceId) { prefs ->
+ prefs[kSelectedAlbum] = selectedAlbum?.id ?: ""
+ prefs[kSelectedAlbumName] = selectedAlbum?.label ?: ""
+ prefs[kShowAlbumName] = showAlbumName
+ }
+
+ ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM)
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar (
+ title = { Text("Widget Configuration") },
+ actions = {
+ IconButton(onClick = {
+ scope.launch {
+ saveConfiguration()
+ onDone()
+ }
+ }) {
+ Icon(Icons.Default.Check, contentDescription = "Close", tint = MaterialTheme.colorScheme.primary)
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding), // Respect the top bar
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
+ when (state) {
+ WidgetConfigState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp))
+ WidgetConfigState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.")
+ else -> {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium)
+
+ // no connection warning
+ if (state == WidgetConfigState.NO_CONNECTION) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.errorContainer)
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Warning,
+ contentDescription = "Warning",
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ text = "No connection to the server is available. Please try again later.",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text("Album")
+ Dropdown(
+ items = availableAlbums,
+ selectedItem = selectedAlbum,
+ onItemSelected = { selectedAlbum = it },
+ enabled = (state != WidgetConfigState.NO_CONNECTION)
+ )
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "Show Album Name")
+ Switch(
+ checked = showAlbumName,
+ onCheckedChange = { showAlbumName = it },
+ enabled = (state != WidgetConfigState.NO_CONNECTION)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt
new file mode 100644
index 0000000000..9595a3b696
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt
@@ -0,0 +1,80 @@
+package app.alextran.immich.widget.model
+
+import android.graphics.Bitmap
+import androidx.datastore.preferences.core.*
+
+// MARK: Immich Entities
+
+enum class AssetType {
+ IMAGE, VIDEO, AUDIO, OTHER
+}
+
+data class Asset(
+ val id: String,
+ val type: AssetType,
+)
+
+data class SearchFilters(
+ var type: AssetType = AssetType.IMAGE,
+ val size: Int = 1,
+ var albumIds: List = listOf(),
+ var isFavorite: Boolean? = null
+)
+
+data class MemoryResult(
+ val id: String,
+ var assets: List,
+ 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}"
+}
diff --git a/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png b/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png
new file mode 100644
index 0000000000..97aceb3ef6
Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-nodpi/memory_preview.png differ
diff --git a/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png b/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png
new file mode 100644
index 0000000000..f94d1bbcd5
Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-nodpi/random_preview.png differ
diff --git a/mobile/android/app/src/main/res/values/strings.xml b/mobile/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..5ac495ebb5
--- /dev/null
+++ b/mobile/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+ Memories
+ Random
+
+ See memories from Immich.
+ View a random image from your library or a specific album.
+
diff --git a/mobile/android/app/src/main/res/xml/memory_widget.xml b/mobile/android/app/src/main/res/xml/memory_widget.xml
new file mode 100644
index 0000000000..611c5aae02
--- /dev/null
+++ b/mobile/android/app/src/main/res/xml/memory_widget.xml
@@ -0,0 +1,9 @@
+
diff --git a/mobile/android/app/src/main/res/xml/random_widget.xml b/mobile/android/app/src/main/res/xml/random_widget.xml
new file mode 100644
index 0000000000..25fb24754f
--- /dev/null
+++ b/mobile/android/app/src/main/res/xml/random_widget.xml
@@ -0,0 +1,13 @@
+
diff --git a/mobile/dcm_global.yaml b/mobile/dcm_global.yaml
index d2465e64b6..c33846e674 100644
--- a/mobile/dcm_global.yaml
+++ b/mobile/dcm_global.yaml
@@ -1 +1 @@
-version: '>=1.29.0 <1.30.0'
+version: '>=1.29.0 <=1.30.0'
diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json
index 03656ce0b4..978a9ba8ad 100644
--- a/mobile/drift_schemas/main/drift_schema_v1.json
+++ b/mobile/drift_schemas/main/drift_schema_v1.json
@@ -1 +1 @@
-{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":7,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":8,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":9,"references":[2,8],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":11,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":12,"references":[1,11],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":13,"references":[11,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":14,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":15,"references":[1,14],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":16,"references":[0,1],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]}
\ No newline at end of file
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0,1],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]}
\ No newline at end of file
diff --git a/mobile/drift_schemas/main/drift_schema_v2.json b/mobile/drift_schemas/main/drift_schema_v2.json
new file mode 100644
index 0000000000..978a9ba8ad
--- /dev/null
+++ b/mobile/drift_schemas/main/drift_schema_v2.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0,1],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]}
\ No newline at end of file
diff --git a/mobile/drift_schemas/main/drift_schema_v3.json b/mobile/drift_schemas/main/drift_schema_v3.json
new file mode 100644
index 0000000000..1acfbaf493
--- /dev/null
+++ b/mobile/drift_schemas/main/drift_schema_v3.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":6,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":7,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":8,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":9,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":10,"references":[2,9],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":11,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":12,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[1,12],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":14,"references":[12,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":15,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":16,"references":[1,15],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":17,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_path","getter_name":"thumbnailPath","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]}
\ No newline at end of file
diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
index e2622fadd1..7d3ed4757e 100644
--- a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
+++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
@@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase {
return rules;
}
- static makeCode(String name, LintOptions options) => LintCode(
+ static LintCode makeCode(String name, LintOptions options) => LintCode(
name: name,
problemMessage: options.json["message"] as String,
errorSeverity: ErrorSeverity.WARNING,
diff --git a/mobile/ios/WidgetExtension/ImageEntry.swift b/mobile/ios/WidgetExtension/ImageEntry.swift
index 5239702d4b..ee371703a8 100644
--- a/mobile/ios/WidgetExtension/ImageEntry.swift
+++ b/mobile/ios/WidgetExtension/ImageEntry.swift
@@ -14,19 +14,6 @@ struct ImageEntry: TimelineEntry {
var deepLink: URL? = nil
}
- // Resizes the stored image to a maximum width of 450 pixels
- mutating func resize() {
- if image == nil || image!.size.height < 450 || image!.size.width < 450 {
- return
- }
-
- image = image?.resized(toWidth: 450)
-
- if image == nil {
- metadata.error = .unableToResize
- }
- }
-
static func build(
api: ImmichAPI,
asset: Asset,
@@ -83,7 +70,10 @@ struct ImageEntry: TimelineEntry {
guard let imageData = try? Data(contentsOf: imageURL),
let metadataJSON = try? Data(contentsOf: metadataURL),
- let decodedMetadata = try? JSONDecoder().decode(Metadata.self, from: metadataJSON)
+ let decodedMetadata = try? JSONDecoder().decode(
+ Metadata.self,
+ from: metadataJSON
+ )
else {
return nil
}
@@ -98,7 +88,7 @@ struct ImageEntry: TimelineEntry {
return nil
}
- static func handleCacheFallback(
+ static func handleError(
for key: String,
error: WidgetError = .fetchFailed
) -> Timeline {
@@ -108,35 +98,32 @@ struct ImageEntry: TimelineEntry {
metadata: EntryMetadata(error: error)
)
- // skip cache if album not found or no login
- // we want to show these errors to the user since without intervention,
+ // use cache if generic failed error
+ // we want to show the other errors to the user since without intervention,
// it will never succeed
- if error != .noLogin && error != .albumNotFound {
- if let cachedEntry = ImageEntry.loadCached(for: key) {
- timelineEntry = cachedEntry
- }
+ if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
+ {
+ timelineEntry = cachedEntry
}
return Timeline(entries: [timelineEntry], policy: .atEnd)
}
+
}
func generateRandomEntries(
api: ImmichAPI,
now: Date,
count: Int,
- albumId: String? = nil,
+ filter: SearchFilter = Album.NONE.filter,
subtitle: String? = nil
)
async throws -> [ImageEntry]
{
var entries: [ImageEntry] = []
- let albumIds = albumId != nil ? [albumId!] : []
- let randomAssets = try await api.fetchSearchResults(
- with: SearchFilters(size: count, albumIds: albumIds)
- )
+ let randomAssets = try await api.fetchSearchResults(with: filter)
await withTaskGroup(of: ImageEntry?.self) { group in
for (dateOffset, asset) in randomAssets.enumerated() {
diff --git a/mobile/ios/WidgetExtension/ImageWidgetView.swift b/mobile/ios/WidgetExtension/ImageWidgetView.swift
index 24072b0607..8e810b051e 100644
--- a/mobile/ios/WidgetExtension/ImageWidgetView.swift
+++ b/mobile/ios/WidgetExtension/ImageWidgetView.swift
@@ -1,6 +1,18 @@
import SwiftUI
import WidgetKit
+extension Image {
+ @ViewBuilder
+ func tintedWidgetImageModifier() -> some View {
+ if #available(iOS 18.0, *) {
+ self
+ .widgetAccentedRenderingMode(.accentedDesaturated)
+ } else {
+ self
+ }
+ }
+}
+
struct ImmichWidgetView: View {
var entry: ImageEntry
@@ -8,6 +20,7 @@ struct ImmichWidgetView: View {
if entry.image == nil {
VStack {
Image("LaunchImage")
+ .tintedWidgetImageModifier()
Text(entry.metadata.error?.errorDescription ?? "")
.minimumScaleFactor(0.25)
.multilineTextAlignment(.center)
@@ -19,7 +32,9 @@ struct ImmichWidgetView: View {
Color.clear.overlay(
Image(uiImage: entry.image!)
.resizable()
+ .tintedWidgetImageModifier()
.scaledToFill()
+
)
VStack {
Spacer()
diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift
index 7cefd9d5ee..36758b824c 100644
--- a/mobile/ios/WidgetExtension/ImmichAPI.swift
+++ b/mobile/ios/WidgetExtension/ImmichAPI.swift
@@ -2,14 +2,20 @@ import Foundation
import SwiftUI
import WidgetKit
+let IMMICH_SHARE_GROUP = "group.app.immich.share"
+
enum WidgetError: Error, Codable {
case noLogin
case fetchFailed
- case unknown
case albumNotFound
+ case noAssetsAvailable
+}
+
+enum FetchError: Error {
case unableToResize
case invalidImage
case invalidURL
+ case fetchFailed
}
extension WidgetError: LocalizedError {
@@ -24,14 +30,8 @@ extension WidgetError: LocalizedError {
case .albumNotFound:
return "Album not found"
- case .invalidURL:
- return "An invalid URL was used"
-
- case .invalidImage:
- return "An invalid image was received"
-
- default:
- return "An unknown error occured"
+ case .noAssetsAvailable:
+ return "No assets available"
}
}
}
@@ -52,10 +52,11 @@ struct Asset: Codable {
}
}
-struct SearchFilters: Codable {
- var type: AssetType = .image
- let size: Int
+struct SearchFilter: Codable {
+ var type = AssetType.image
+ var size = 1
var albumIds: [String] = []
+ var isFavorite: Bool? = nil
}
struct MemoryResult: Codable {
@@ -70,12 +71,35 @@ struct MemoryResult: Codable {
let data: MemoryData
}
-struct Album: Codable {
+struct Album: Codable, Equatable {
let id: String
let albumName: String
-}
-let IMMICH_SHARE_GROUP = "group.app.immich.share"
+ static let NONE = Album(id: "NONE", albumName: "None")
+ static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
+
+ var filter: SearchFilter {
+ switch self {
+ case Album.NONE:
+ return SearchFilter()
+ case Album.FAVORITES:
+ return SearchFilter(isFavorite: true)
+
+ // regular album
+ default:
+ return SearchFilter(albumIds: [id])
+ }
+ }
+
+ var isVirtual: Bool {
+ switch self {
+ case Album.NONE, Album.FAVORITES:
+ return true
+ default:
+ return false
+ }
+ }
+}
// MARK: API
@@ -132,7 +156,8 @@ class ImmichAPI {
return components?.url
}
- func fetchSearchResults(with filters: SearchFilters) async throws
+ func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
+ async throws
-> [Asset]
{
// get URL
@@ -178,7 +203,7 @@ class ImmichAPI {
return try JSONDecoder().decode([MemoryResult].self, from: data)
}
- func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
+ func fetchImage(asset: Asset) async throws(FetchError) -> UIImage {
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
@@ -199,7 +224,7 @@ class ImmichAPI {
let decodeOptions: [NSString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
- kCGImageSourceThumbnailMaxPixelSize: 400,
+ kCGImageSourceThumbnailMaxPixelSize: 512,
kCGImageSourceCreateThumbnailWithTransform: true,
]
diff --git a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift
index da7b887c37..d0a3e8c29d 100644
--- a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift
+++ b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift
@@ -23,26 +23,27 @@ struct ImmichMemoryProvider: TimelineProvider {
Task {
guard let api = try? await ImmichAPI() else {
- completion(ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!)
+ completion(
+ ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
+ )
return
}
guard let memories = try? await api.fetchMemory(for: Date.now)
else {
- completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
+ completion(ImageEntry.handleError(for: cacheKey).entries.first!)
return
}
for memory in memories {
if let asset = memory.assets.first(where: { $0.type == .image }),
- var entry = try? await ImageEntry.build(
+ let entry = try? await ImageEntry.build(
api: api,
asset: asset,
dateOffset: 0,
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
)
{
- entry.resize()
completion(entry)
return
}
@@ -50,20 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
// fallback to random image
guard
- let randomImage = try? await api.fetchSearchResults(
- with: SearchFilters(size: 1)
- ).first,
- var imageEntry = try? await ImageEntry.build(
+ let randomImage = try? await api.fetchSearchResults().first,
+ let imageEntry = try? await ImageEntry.build(
api: api,
asset: randomImage,
dateOffset: 0
)
else {
- completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
+ completion(ImageEntry.handleError(for: cacheKey).entries.first!)
return
}
- imageEntry.resize()
completion(imageEntry)
}
}
@@ -80,7 +78,7 @@ struct ImmichMemoryProvider: TimelineProvider {
guard let api = try? await ImmichAPI() else {
completion(
- ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
+ ImageEntry.handleError(for: cacheKey, error: .noLogin)
)
return
}
@@ -119,25 +117,28 @@ struct ImmichMemoryProvider: TimelineProvider {
// If we didnt add any memory images (some failure occured or no images in memory),
// default to 12 hours of random photos
if entries.count == 0 {
- entries.append(
- contentsOf: (try? await generateRandomEntries(
+ // this must be a do/catch since we need to
+ // distinguish between a network fail and an empty search
+ do {
+ let search = try await generateRandomEntries(
api: api,
now: now,
count: 12
- )) ?? []
- )
- }
+ )
- // If we fail to fetch images, we still want to add an entry
- // with a nil image and an error
- if entries.count == 0 {
- completion(ImageEntry.handleCacheFallback(for: cacheKey))
- return
- }
+ // Load or save a cached asset for when network conditions are bad
+ if search.count == 0 {
+ completion(
+ ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
+ )
+ return
+ }
- // Resize all images to something that can be stored by iOS
- for i in entries.indices {
- entries[i].resize()
+ entries.append(contentsOf: search)
+ } catch {
+ completion(ImageEntry.handleError(for: cacheKey))
+ return
+ }
}
// cache the last image
diff --git a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift
index 8f9143cedd..37f3c5e596 100644
--- a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift
+++ b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift
@@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
struct AlbumQuery: EntityQuery {
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
- // use cached albums to search
- var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
- albums.insert(NO_ALBUM, at: 0)
-
- return albums.filter {
+ return await suggestedEntities().filter {
identifiers.contains($0.id)
}
}
- func suggestedEntities() async throws -> [Album] {
- var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
- albums.insert(NO_ALBUM, at: 0)
+ func suggestedEntities() async -> [Album] {
+ let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
- return albums
+ let options =
+ [
+ NONE,
+ FAVORITES,
+ ] + albums
+
+ return options
}
}
@@ -35,8 +36,6 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
}
}
-let NO_ALBUM = Album(id: "NONE", albumName: "None")
-
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Select Album" }
static var description: IntentDescription {
@@ -64,26 +63,25 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
-> ImageEntry
{
let cacheKey = "random_none_\(context.family.rawValue)"
-
+
guard let api = try? await ImmichAPI() else {
- return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!
+ return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
+ .first!
}
guard
let randomImage = try? await api.fetchSearchResults(
- with: SearchFilters(size: 1)
+ with: Album.NONE.filter
).first,
- var entry = try? await ImageEntry.build(
+ let entry = try? await ImageEntry.build(
api: api,
asset: randomImage,
dateOffset: 0
)
else {
- return ImageEntry.handleCacheFallback(for: cacheKey).entries.first!
+ return ImageEntry.handleError(for: cacheKey).entries.first!
}
- entry.resize()
-
return entry
}
@@ -97,52 +95,36 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
let now = Date()
// nil if album is NONE or nil
- let albumId =
- configuration.album?.id != "NONE" ? configuration.album?.id : nil
- let albumName: String? =
- albumId != nil ? configuration.album?.albumName : nil
+ let album = configuration.album ?? Album.NONE
+ let albumName = album.isVirtual ? nil : album.albumName
- let cacheKey = "random_\(albumId ?? "none")_\(context.family.rawValue)"
+ let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
// If we don't have a server config, return an entry with an error
guard let api = try? await ImmichAPI() else {
- return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
- }
-
- if albumId != nil {
- // make sure the album exists on server, otherwise show error
- guard let albums = try? await api.fetchAlbums() else {
- return ImageEntry.handleCacheFallback(for: cacheKey)
- }
-
- if !albums.contains(where: { $0.id == albumId }) {
- return ImageEntry.handleCacheFallback(
- for: cacheKey,
- error: .albumNotFound
- )
- }
+ return ImageEntry.handleError(for: cacheKey, error: .noLogin)
}
// build entries
- entries.append(
- contentsOf: (try? await generateRandomEntries(
+ // this must be a do/catch since we need to
+ // distinguish between a network fail and an empty search
+ do {
+ let search = try await generateRandomEntries(
api: api,
now: now,
count: 12,
- albumId: albumId,
+ filter: album.filter,
subtitle: configuration.showAlbumName ? albumName : nil
- ))
- ?? []
- )
+ )
- // Load or save a cached asset for when network conditions are bad
- if entries.count == 0 {
- return ImageEntry.handleCacheFallback(for: cacheKey)
- }
+ // Load or save a cached asset for when network conditions are bad
+ if search.count == 0 {
+ return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
+ }
- // Resize all images to something that can be stored by iOS
- for i in entries.indices {
- entries[i].resize()
+ entries.append(contentsOf: search)
+ } catch {
+ return ImageEntry.handleError(for: cacheKey)
}
// cache the last image
diff --git a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json
deleted file mode 100644
index 7391713b6f..0000000000
--- a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json
+++ /dev/null
@@ -1 +0,0 @@
-{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}}
\ No newline at end of file
diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart
index 6d98152efc..206fbbb28f 100644
--- a/mobile/lib/constants/constants.dart
+++ b/mobile/lib/constants/constants.dart
@@ -16,6 +16,11 @@ const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
// Secure storage keys
const String kSecuredPinCode = "secured_pin_code";
+// background_downloader task groups
+const String kManualUploadGroup = 'manual_upload_group';
+const String kBackupGroup = 'backup_group';
+const String kBackupLivePhotoGroup = 'backup_live_photo_group';
+
// Timeline constants
const int kTimelineNoneSegmentSize = 120;
const int kTimelineAssetLoadBatchSize = 256;
@@ -28,7 +33,11 @@ const String appShareGroupId = "group.app.immich.share";
// add widget identifiers here for new widgets
// these are used to force a widget refresh
-const List kWidgetNames = [
- 'com.immich.widget.random',
- 'com.immich.widget.memory',
+// (iOSName, androidFQDN)
+const List<(String, String)> kWidgetNames = [
+ ('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
+ ('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
];
+
+const double kUploadStatusFailed = -1.0;
+const double kUploadStatusCanceled = -2.0;
diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart
index 7cafca9116..a199bce129 100644
--- a/mobile/lib/domain/models/album/album.model.dart
+++ b/mobile/lib/domain/models/album/album.model.dart
@@ -86,4 +86,32 @@ class RemoteAlbum {
assetCount.hashCode ^
ownerName.hashCode;
}
+
+ RemoteAlbum copyWith({
+ String? id,
+ String? name,
+ String? ownerId,
+ String? description,
+ DateTime? createdAt,
+ DateTime? updatedAt,
+ String? thumbnailAssetId,
+ bool? isActivityEnabled,
+ AlbumAssetOrder? order,
+ int? assetCount,
+ String? ownerName,
+ }) {
+ return RemoteAlbum(
+ id: id ?? this.id,
+ name: name ?? this.name,
+ ownerId: ownerId ?? this.ownerId,
+ description: description ?? this.description,
+ createdAt: createdAt ?? this.createdAt,
+ updatedAt: updatedAt ?? this.updatedAt,
+ thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
+ isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
+ order: order ?? this.order,
+ assetCount: assetCount ?? this.assetCount,
+ ownerName: ownerName ?? this.ownerName,
+ );
+ }
}
diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart
index 0c3e8fa942..3466a0f25b 100644
--- a/mobile/lib/domain/models/asset/local_asset.model.dart
+++ b/mobile/lib/domain/models/asset/local_asset.model.dart
@@ -45,6 +45,7 @@ class LocalAsset extends BaseAsset {
}''';
}
+ // Not checking for remoteId here
@override
bool operator ==(Object other) {
if (other is! LocalAsset) return false;
diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart
index 9e4cfa1f19..760a16170b 100644
--- a/mobile/lib/domain/models/asset/remote_asset.model.dart
+++ b/mobile/lib/domain/models/asset/remote_asset.model.dart
@@ -14,6 +14,8 @@ class RemoteAsset extends BaseAsset {
final String? thumbHash;
final AssetVisibility visibility;
final String ownerId;
+ final String? stackId;
+ final int stackCount;
const RemoteAsset({
required this.id,
@@ -31,6 +33,8 @@ class RemoteAsset extends BaseAsset {
this.thumbHash,
this.visibility = AssetVisibility.timeline,
super.livePhotoVideoId,
+ this.stackId,
+ this.stackCount = 0,
});
@override
@@ -56,9 +60,14 @@ class RemoteAsset extends BaseAsset {
isFavorite: $isFavorite,
thumbHash: ${thumbHash ?? ""},
visibility: $visibility,
+ stackId: ${stackId ?? ""},
+ stackCount: $stackCount,
+ checksum: $checksum,
+ livePhotoVideoId: ${livePhotoVideoId ?? ""},
}''';
}
+ // Not checking for localId here
@override
bool operator ==(Object other) {
if (other is! RemoteAsset) return false;
@@ -67,7 +76,9 @@ class RemoteAsset extends BaseAsset {
id == other.id &&
ownerId == other.ownerId &&
thumbHash == other.thumbHash &&
- visibility == other.visibility;
+ visibility == other.visibility &&
+ stackId == other.stackId &&
+ stackCount == other.stackCount;
}
@override
@@ -77,7 +88,9 @@ class RemoteAsset extends BaseAsset {
ownerId.hashCode ^
localId.hashCode ^
thumbHash.hashCode ^
- visibility.hashCode;
+ visibility.hashCode ^
+ stackId.hashCode ^
+ stackCount.hashCode;
RemoteAsset copyWith({
String? id,
@@ -95,6 +108,8 @@ class RemoteAsset extends BaseAsset {
String? thumbHash,
AssetVisibility? visibility,
String? livePhotoVideoId,
+ String? stackId,
+ int? stackCount,
}) {
return RemoteAsset(
id: id ?? this.id,
@@ -112,6 +127,8 @@ class RemoteAsset extends BaseAsset {
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
+ stackId: stackId ?? this.stackId,
+ stackCount: stackCount ?? this.stackCount,
);
}
}
diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart
index ba2a43428f..38d1c7ef7d 100644
--- a/mobile/lib/domain/models/memory.model.dart
+++ b/mobile/lib/domain/models/memory.model.dart
@@ -124,7 +124,21 @@ class DriftMemory {
@override
String toString() {
- return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
+ return '''Memory {
+ id: $id,
+ createdAt: $createdAt,
+ updatedAt: $updatedAt,
+ deletedAt: ${deletedAt ?? ""},
+ ownerId: $ownerId,
+ type: $type,
+ data: $data,
+ isSaved: $isSaved,
+ memoryAt: $memoryAt,
+ seenAt: ${seenAt ?? ""},
+ showAt: ${showAt ?? ""},
+ hideAt: ${hideAt ?? ""},
+ assets: $assets
+}''';
}
@override
diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart
index 10453f768d..d9eee9ae06 100644
--- a/mobile/lib/domain/models/person.model.dart
+++ b/mobile/lib/domain/models/person.model.dart
@@ -1,7 +1,8 @@
import 'dart:convert';
-class Person {
- const Person({
+// TODO: Remove PersonDto once Isar is removed
+class PersonDto {
+ const PersonDto({
required this.id,
this.birthDate,
required this.isHidden,
@@ -22,7 +23,7 @@ class Person {
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
}
- Person copyWith({
+ PersonDto copyWith({
String? id,
DateTime? birthDate,
bool? isHidden,
@@ -30,7 +31,7 @@ class Person {
String? thumbnailPath,
DateTime? updatedAt,
}) {
- return Person(
+ return PersonDto(
id: id ?? this.id,
birthDate: birthDate ?? this.birthDate,
isHidden: isHidden ?? this.isHidden,
@@ -51,8 +52,8 @@ class Person {
};
}
- factory Person.fromMap(Map map) {
- return Person(
+ factory PersonDto.fromMap(Map map) {
+ return PersonDto(
id: map['id'] as String,
birthDate: map['birthDate'] != null
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
@@ -68,11 +69,11 @@ class Person {
String toJson() => json.encode(toMap());
- factory Person.fromJson(String source) =>
- Person.fromMap(json.decode(source) as Map);
+ factory PersonDto.fromJson(String source) =>
+ PersonDto.fromMap(json.decode(source) as Map);
@override
- bool operator ==(covariant Person other) {
+ bool operator ==(covariant PersonDto other) {
if (identical(this, other)) return true;
return other.id == id &&
@@ -93,3 +94,109 @@ class Person {
updatedAt.hashCode;
}
}
+
+// Model for a person stored in the server
+class Person {
+ final String id;
+ final DateTime createdAt;
+ final DateTime updatedAt;
+ final String ownerId;
+ final String name;
+ final String? faceAssetId;
+ final String thumbnailPath;
+ final bool isFavorite;
+ final bool isHidden;
+ final String? color;
+ final DateTime? birthDate;
+
+ const Person({
+ required this.id,
+ required this.createdAt,
+ required this.updatedAt,
+ required this.ownerId,
+ required this.name,
+ this.faceAssetId,
+ required this.thumbnailPath,
+ required this.isFavorite,
+ required this.isHidden,
+ required this.color,
+ this.birthDate,
+ });
+
+ Person copyWith({
+ String? id,
+ DateTime? createdAt,
+ DateTime? updatedAt,
+ String? ownerId,
+ String? name,
+ String? faceAssetId,
+ String? thumbnailPath,
+ bool? isFavorite,
+ bool? isHidden,
+ String? color,
+ DateTime? birthDate,
+ }) {
+ return Person(
+ id: id ?? this.id,
+ createdAt: createdAt ?? this.createdAt,
+ updatedAt: updatedAt ?? this.updatedAt,
+ ownerId: ownerId ?? this.ownerId,
+ name: name ?? this.name,
+ faceAssetId: faceAssetId ?? this.faceAssetId,
+ thumbnailPath: thumbnailPath ?? this.thumbnailPath,
+ isFavorite: isFavorite ?? this.isFavorite,
+ isHidden: isHidden ?? this.isHidden,
+ color: color ?? this.color,
+ birthDate: birthDate ?? this.birthDate,
+ );
+ }
+
+ @override
+ String toString() {
+ return '''Person {
+ id: $id,
+ createdAt: $createdAt,
+ updatedAt: $updatedAt,
+ ownerId: $ownerId,
+ name: $name,
+ faceAssetId: ${faceAssetId ?? ""},
+ thumbnailPath: $thumbnailPath,
+ isFavorite: $isFavorite,
+ isHidden: $isHidden,
+ color: ${color ?? ""},
+ birthDate: ${birthDate ?? ""}
+}''';
+ }
+
+ @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;
+ }
+}
diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart
new file mode 100644
index 0000000000..e8c9429432
--- /dev/null
+++ b/mobile/lib/domain/models/search_result.model.dart
@@ -0,0 +1,38 @@
+import 'package:collection/collection.dart';
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+
+class SearchResult {
+ final List assets;
+ final int? nextPage;
+
+ const SearchResult({
+ required this.assets,
+ this.nextPage,
+ });
+
+ int get totalAssets => assets.length;
+
+ SearchResult copyWith({
+ List? 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;
+}
diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart
index 5404eb8f42..6a449a1eb8 100644
--- a/mobile/lib/domain/models/stack.model.dart
+++ b/mobile/lib/domain/models/stack.model.dart
@@ -1,5 +1,3 @@
-import 'dart:convert';
-
// Model for a stack stored in the server
class Stack {
final String id;
@@ -32,34 +30,15 @@ class Stack {
);
}
- Map toMap() {
- return {
- 'id': id,
- 'createdAt': createdAt.millisecondsSinceEpoch,
- 'updatedAt': updatedAt.millisecondsSinceEpoch,
- 'ownerId': ownerId,
- 'primaryAssetId': primaryAssetId,
- };
- }
-
- factory Stack.fromMap(Map 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);
-
@override
String toString() {
- return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
+ return '''Stack {
+ id: $id,
+ createdAt: $createdAt,
+ updatedAt: $updatedAt,
+ ownerId: $ownerId,
+ primaryAssetId: $primaryAssetId
+}''';
}
@override
@@ -82,3 +61,27 @@ class Stack {
primaryAssetId.hashCode;
}
}
+
+class StackResponse {
+ final String id;
+ final String primaryAssetId;
+ final List 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;
+}
diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart
index a96e8d3bce..305b3f3387 100644
--- a/mobile/lib/domain/models/store.model.dart
+++ b/mobile/lib/domain/models/store.model.dart
@@ -68,7 +68,10 @@ enum StoreKey {
manageLocalMediaAndroid._(137),
// Experimental stuff
- photoManagerCustomFilter._(1000);
+ photoManagerCustomFilter._(1000),
+ betaPromptShown._(1001),
+ betaTimeline._(1002),
+ enableBackup._(1003);
const StoreKey._(this.id);
final int id;
diff --git a/mobile/lib/domain/models/timeline.model.dart b/mobile/lib/domain/models/timeline.model.dart
index 4a49708b74..f3b688b8b8 100644
--- a/mobile/lib/domain/models/timeline.model.dart
+++ b/mobile/lib/domain/models/timeline.model.dart
@@ -1,3 +1,5 @@
+import 'package:immich_mobile/domain/utils/event_stream.dart';
+
enum GroupAssetsBy {
day,
month,
@@ -38,3 +40,7 @@ class TimeBucket extends Bucket {
@override
int get hashCode => super.hashCode ^ date.hashCode;
}
+
+class TimelineReloadEvent extends Event {
+ const TimelineReloadEvent();
+}
diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart
index abf2e5620b..6cafd8d149 100644
--- a/mobile/lib/domain/models/user.model.dart
+++ b/mobile/lib/domain/models/user.model.dart
@@ -1,3 +1,6 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
// TODO: Rename to User once Isar is removed
@@ -123,3 +126,88 @@ quotaSizeInBytes: $quotaSizeInBytes,
quotaUsageInBytes.hashCode ^
quotaSizeInBytes.hashCode;
}
+
+class PartnerUserDto {
+ final String id;
+ final String email;
+ final String name;
+ final bool inTimeline;
+
+ final String? profileImagePath;
+
+ const PartnerUserDto({
+ required this.id,
+ required this.email,
+ required this.name,
+ required this.inTimeline,
+ this.profileImagePath,
+ });
+
+ PartnerUserDto copyWith({
+ String? id,
+ String? email,
+ String? name,
+ bool? inTimeline,
+ String? profileImagePath,
+ }) {
+ return PartnerUserDto(
+ id: id ?? this.id,
+ email: email ?? this.email,
+ name: name ?? this.name,
+ inTimeline: inTimeline ?? this.inTimeline,
+ profileImagePath: profileImagePath ?? this.profileImagePath,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'id': id,
+ 'email': email,
+ 'name': name,
+ 'inTimeline': inTimeline,
+ 'profileImagePath': profileImagePath,
+ };
+ }
+
+ factory PartnerUserDto.fromMap(Map map) {
+ return PartnerUserDto(
+ id: map['id'] as String,
+ email: map['email'] as String,
+ name: map['name'] as String,
+ inTimeline: map['inTimeline'] as bool,
+ profileImagePath: map['profileImagePath'] != null
+ ? map['profileImagePath'] as String
+ : null,
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory PartnerUserDto.fromJson(String source) =>
+ PartnerUserDto.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() {
+ return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)';
+ }
+
+ @override
+ bool operator ==(covariant PartnerUserDto other) {
+ if (identical(this, other)) return true;
+
+ return other.id == id &&
+ other.email == email &&
+ other.name == name &&
+ other.inTimeline == inTimeline &&
+ other.profileImagePath == profileImagePath;
+ }
+
+ @override
+ int get hashCode {
+ return id.hashCode ^
+ email.hashCode ^
+ name.hashCode ^
+ inTimeline.hashCode ^
+ profileImagePath.hashCode;
+ }
+}
diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart
index 2c9b493187..63b1aad8c1 100644
--- a/mobile/lib/domain/services/asset.service.dart
+++ b/mobile/lib/domain/services/asset.service.dart
@@ -24,6 +24,17 @@ class AssetService {
: _remoteAssetRepository.watchAsset(id);
}
+ Future> 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 getExif(BaseAsset asset) async {
if (!asset.hasRemote) {
return null;
diff --git a/mobile/lib/domain/services/local_album.service.dart b/mobile/lib/domain/services/local_album.service.dart
index 9af12ce595..79cc58f3e0 100644
--- a/mobile/lib/domain/services/local_album.service.dart
+++ b/mobile/lib/domain/services/local_album.service.dart
@@ -7,11 +7,15 @@ class LocalAlbumService {
const LocalAlbumService(this._repository);
- Future> getAll() {
- return _repository.getAll();
+ Future> getAll({Set sortBy = const {}}) {
+ return _repository.getAll(sortBy: sortBy);
}
Future getThumbnail(String albumId) {
return _repository.getThumbnail(albumId);
}
+
+ Future update(LocalAlbum album) {
+ return _repository.upsert(album);
+ }
}
diff --git a/mobile/lib/domain/services/partner.service.dart b/mobile/lib/domain/services/partner.service.dart
new file mode 100644
index 0000000000..065560c4be
--- /dev/null
+++ b/mobile/lib/domain/services/partner.service.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/domain/models/user.model.dart';
+import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
+import 'package:immich_mobile/repositories/partner_api.repository.dart';
+
+class DriftPartnerService {
+ final DriftPartnerRepository _driftPartnerRepository;
+ final PartnerApiRepository _partnerApiRepository;
+
+ const DriftPartnerService(
+ this._driftPartnerRepository,
+ this._partnerApiRepository,
+ );
+
+ Future> getSharedWith(String userId) {
+ return _driftPartnerRepository.getSharedWith(userId);
+ }
+
+ Future> getSharedBy(String userId) {
+ return _driftPartnerRepository.getSharedBy(userId);
+ }
+
+ Future> getAvailablePartners(
+ String currentUserId,
+ ) async {
+ final otherUsers =
+ await _driftPartnerRepository.getAvailablePartners(currentUserId);
+ final currentPartners =
+ await _driftPartnerRepository.getSharedBy(currentUserId);
+ final available = otherUsers.where((user) {
+ return !currentPartners.any((partner) => partner.id == user.id);
+ }).toList();
+
+ return available;
+ }
+
+ Future toggleShowInTimeline(String partnerId, String userId) async {
+ final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
+ if (partner == null) {
+ debugPrint("Partner not found: $partnerId for user: $userId");
+ return;
+ }
+
+ await _partnerApiRepository.update(
+ partnerId,
+ inTimeline: !partner.inTimeline,
+ );
+
+ await _driftPartnerRepository.toggleShowInTimeline(partner, userId);
+ }
+
+ Future addPartner(String partnerId, String userId) async {
+ await _partnerApiRepository.create(partnerId);
+ await _driftPartnerRepository.create(partnerId, userId);
+ }
+
+ Future removePartner(String partnerId, String userId) async {
+ await _partnerApiRepository.delete(partnerId);
+ await _driftPartnerRepository.delete(partnerId, userId);
+ }
+}
diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart
index ae9e8b5336..ebb24d5fe5 100644
--- a/mobile/lib/domain/services/remote_album.service.dart
+++ b/mobile/lib/domain/services/remote_album.service.dart
@@ -1,4 +1,8 @@
+import 'dart:async';
+
import 'package:immich_mobile/domain/models/album/album.model.dart';
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
@@ -10,6 +14,10 @@ class RemoteAlbumService {
const RemoteAlbumService(this._repository, this._albumApiRepository);
+ Stream watchAlbum(String albumId) {
+ return _repository.watchAlbum(albumId);
+ }
+
Future> getAll() {
return _repository.getAll();
}
@@ -75,4 +83,68 @@ class RemoteAlbumService {
return album;
}
+
+ Future updateAlbum(
+ String albumId, {
+ String? name,
+ String? description,
+ String? thumbnailAssetId,
+ bool? isActivityEnabled,
+ AlbumAssetOrder? order,
+ }) async {
+ final updatedAlbum = await _albumApiRepository.updateAlbum(
+ albumId,
+ name: name,
+ description: description,
+ thumbnailAssetId: thumbnailAssetId,
+ isActivityEnabled: isActivityEnabled,
+ order: order,
+ );
+
+ // Update the local database
+ await _repository.update(updatedAlbum);
+
+ return updatedAlbum;
+ }
+
+ FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
+ return _repository.getDateRange(albumId);
+ }
+
+ Future> getSharedUsers(String albumId) {
+ return _repository.getSharedUsers(albumId);
+ }
+
+ Future> getAssets(String albumId) {
+ return _repository.getAssets(albumId);
+ }
+
+ Future addAssets({
+ required String albumId,
+ required List assetIds,
+ }) async {
+ final album = await _albumApiRepository.addAssets(
+ albumId,
+ assetIds,
+ );
+
+ await _repository.addAssets(albumId, album.added);
+
+ return album.added.length;
+ }
+
+ Future deleteAlbum(String albumId) async {
+ await _albumApiRepository.deleteAlbum(albumId);
+
+ await _repository.deleteAlbum(albumId);
+ }
+
+ Future addUsers({
+ required String albumId,
+ required List userIds,
+ }) async {
+ await _albumApiRepository.addUsers(albumId, userIds);
+
+ return _repository.addUsers(albumId, userIds);
+ }
}
diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart
new file mode 100644
index 0000000000..052a2ca9da
--- /dev/null
+++ b/mobile/lib/domain/services/search.service.dart
@@ -0,0 +1,92 @@
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+import 'package:immich_mobile/domain/models/search_result.model.dart';
+import 'package:immich_mobile/extensions/string_extensions.dart';
+import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
+import 'package:immich_mobile/models/search/search_filter.model.dart';
+import 'package:logging/logging.dart';
+import 'package:openapi/api.dart' as api show AssetVisibility;
+import 'package:openapi/api.dart' hide AssetVisibility;
+
+class SearchService {
+ final _log = Logger("SearchService");
+ final SearchApiRepository _searchApiRepository;
+
+ SearchService(this._searchApiRepository);
+
+ Future?> 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 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'),
+ };
+}
diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart
index 359ce8cf60..b1e991b348 100644
--- a/mobile/lib/domain/services/store.service.dart
+++ b/mobile/lib/domain/services/store.service.dart
@@ -93,6 +93,8 @@ class StoreService {
await _storeRepository.deleteAll();
_cache.clear();
}
+
+ bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
}
class StoreKeyNotFoundException implements Exception {
diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart
index 6183865041..9a7d91ced9 100644
--- a/mobile/lib/domain/services/sync_stream.service.dart
+++ b/mobile/lib/domain/services/sync_stream.service.dart
@@ -240,6 +240,10 @@ class SyncStreamService {
return _syncStreamRepository.deleteUserMetadatasV1(
data.cast(),
);
+ case SyncEntityType.personV1:
+ return _syncStreamRepository.updatePeopleV1(data.cast());
+ case SyncEntityType.personDeleteV1:
+ return _syncStreamRepository.deletePeopleV1(data.cast());
default:
_logger.warning("Unknown sync data type: $type");
}
diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart
index 14a854a760..0d31f06e74 100644
--- a/mobile/lib/domain/services/timeline.service.dart
+++ b/mobile/lib/domain/services/timeline.service.dart
@@ -65,6 +65,9 @@ class TimelineFactory {
TimelineService place(String place) =>
TimelineService(_timelineRepository.place(place, groupBy));
+
+ TimelineService fromAssets(List assets) =>
+ TimelineService(_timelineRepository.fromAssets(assets));
}
class TimelineService {
diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart
index c71f1a8315..4a44c4d8f2 100644
--- a/mobile/lib/domain/utils/background_sync.dart
+++ b/mobile/lib/domain/utils/background_sync.dart
@@ -4,13 +4,24 @@ import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart';
+typedef SyncCallback = void Function();
+typedef SyncErrorCallback = void Function(String error);
+
class BackgroundSyncManager {
+ final SyncCallback? onRemoteSyncStart;
+ final SyncCallback? onRemoteSyncComplete;
+ final SyncErrorCallback? onRemoteSyncError;
+
Cancelable? _syncTask;
Cancelable? _syncWebsocketTask;
Cancelable? _deviceAlbumSyncTask;
Cancelable? _hashTask;
- BackgroundSyncManager();
+ BackgroundSyncManager({
+ this.onRemoteSyncStart,
+ this.onRemoteSyncComplete,
+ this.onRemoteSyncError,
+ });
Future cancel() {
final futures = [];
@@ -72,10 +83,16 @@ class BackgroundSyncManager {
return _syncTask!.future;
}
+ onRemoteSyncStart?.call();
+
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
);
return _syncTask!.whenComplete(() {
+ onRemoteSyncComplete?.call();
+ _syncTask = null;
+ }).catchError((error) {
+ onRemoteSyncError?.call(error.toString());
_syncTask = null;
});
}
@@ -84,14 +101,18 @@ class BackgroundSyncManager {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
-
- _syncWebsocketTask = runInIsolateGentle(
- computation: (ref) => ref
- .read(syncStreamServiceProvider)
- .handleWsAssetUploadReadyV1Batch(batchData),
- );
+ _syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData);
return _syncWebsocketTask!.whenComplete(() {
_syncWebsocketTask = null;
});
}
}
+
+Cancelable _handleWsAssetUploadReadyV1Batch(
+ List batchData,
+) =>
+ runInIsolateGentle(
+ computation: (ref) => ref
+ .read(syncStreamServiceProvider)
+ .handleWsAssetUploadReadyV1Batch(batchData),
+ );
diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart
index 65ee17e12b..e728ece58b 100644
--- a/mobile/lib/domain/utils/event_stream.dart
+++ b/mobile/lib/domain/utils/event_stream.dart
@@ -1,17 +1,9 @@
import 'dart:async';
-sealed class Event {
+class Event {
const Event();
}
-class TimelineReloadEvent extends Event {
- const TimelineReloadEvent();
-}
-
-class ViewerOpenBottomSheetEvent extends Event {
- const ViewerOpenBottomSheetEvent();
-}
-
class EventStream {
EventStream._();
diff --git a/mobile/lib/entities/backup_album.entity.dart b/mobile/lib/entities/backup_album.entity.dart
index 4d4d7b3aa3..1e96c0452e 100644
--- a/mobile/lib/entities/backup_album.entity.dart
+++ b/mobile/lib/entities/backup_album.entity.dart
@@ -13,6 +13,18 @@ class BackupAlbum {
BackupAlbum(this.id, this.lastBackup, this.selection);
Id get isarId => fastHash(id);
+
+ BackupAlbum copyWith({
+ String? id,
+ DateTime? lastBackup,
+ BackupSelection? selection,
+ }) {
+ return BackupAlbum(
+ id ?? this.id,
+ lastBackup ?? this.lastBackup,
+ selection ?? this.selection,
+ );
+ }
}
enum BackupSelection {
diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart
index 69a9c3b347..7bb194cdae 100644
--- a/mobile/lib/extensions/build_context_extensions.dart
+++ b/mobile/lib/extensions/build_context_extensions.dart
@@ -33,6 +33,10 @@ extension ContextHelper on BuildContext {
// Returns the current Primary color of the Theme
Color get primaryColor => themeData.colorScheme.primary;
+ Color get logoYellow => const Color.fromARGB(255, 255, 184, 0);
+ Color get logoRed => const Color.fromARGB(255, 230, 65, 30);
+ Color get logoPink => const Color.fromARGB(255, 222, 127, 179);
+ Color get logoGreen => const Color.fromARGB(255, 49, 164, 82);
// Returns the Scaffold background color of the Theme
Color get scaffoldBackgroundColor => colorScheme.surface;
diff --git a/mobile/lib/extensions/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart
index 14d89e2755..e23bf5210f 100644
--- a/mobile/lib/extensions/datetime_extensions.dart
+++ b/mobile/lib/extensions/datetime_extensions.dart
@@ -1,3 +1,6 @@
+import 'dart:ui';
+import 'package:easy_localization/easy_localization.dart';
+
extension TimeAgoExtension on DateTime {
/// Displays the time difference of this [DateTime] object to the current time as a [String]
String timeAgo({bool numericDates = true}) {
@@ -35,3 +38,56 @@ extension TimeAgoExtension on DateTime {
return '${(difference.inDays / 365).floor()} years ago';
}
}
+
+/// Extension to format date ranges according to UI requirements
+extension DateRangeFormatting on DateTime {
+ /// Formats a date range according to specific rules:
+ /// - Single date of this year: "Aug 28"
+ /// - Single date of other year: "Aug 28, 2023"
+ /// - Date range of this year: "Mar 23-May 31"
+ /// - Date range of other year: "Aug 28 - Sep 30, 2023"
+ /// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
+ static String formatDateRange(
+ DateTime startDate,
+ DateTime endDate,
+ Locale? locale,
+ ) {
+ final now = DateTime.now();
+ final currentYear = now.year;
+ final localeString = locale?.toString() ?? 'en_US';
+
+ // Check if it's a single date (same day)
+ if (startDate.year == endDate.year &&
+ startDate.month == endDate.month &&
+ startDate.day == endDate.day) {
+ if (startDate.year == currentYear) {
+ // Single date of this year: "Aug 28"
+ return DateFormat.MMMd(localeString).format(startDate);
+ } else {
+ // Single date of other year: "Aug 28, 2023"
+ return DateFormat.yMMMd(localeString).format(startDate);
+ }
+ }
+
+ // It's a date range
+ if (startDate.year == endDate.year) {
+ // Same year
+ if (startDate.year == currentYear) {
+ // Date range of this year: "Mar 23-May 31"
+ final startFormatted = DateFormat.MMMd(localeString).format(startDate);
+ final endFormatted = DateFormat.MMMd(localeString).format(endDate);
+ return '$startFormatted - $endFormatted';
+ } else {
+ // Date range of other year: "Aug 28 - Sep 30, 2023"
+ final startFormatted = DateFormat.MMMd(localeString).format(startDate);
+ final endFormatted = DateFormat.MMMd(localeString).format(endDate);
+ return '$startFormatted - $endFormatted, ${startDate.year}';
+ }
+ } else {
+ // Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
+ final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
+ final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
+ return '$startFormatted - $endFormatted';
+ }
+ }
+}
diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift
index e07edbc0c8..3dc7221c15 100644
--- a/mobile/lib/infrastructure/entities/merged_asset.drift
+++ b/mobile/lib/infrastructure/entities/merged_asset.drift
@@ -1,5 +1,6 @@
import 'remote_asset.entity.dart';
import 'local_asset.entity.dart';
+import 'stack.entity.dart';
mergedAsset: SELECT * FROM
(
@@ -18,13 +19,33 @@ mergedAsset: SELECT * FROM
rae.checksum,
rae.owner_id,
rae.live_photo_video_id,
- 0 as orientation
+ 0 as orientation,
+ rae.stack_id,
+ COALESCE(stack_count.total_count, 0) AS stack_count
FROM
remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
+ LEFT JOIN
+ stack_entity se ON rae.stack_id = se.id
+ LEFT JOIN
+ (SELECT
+ stack_id,
+ COUNT(*) AS total_count
+ FROM remote_asset_entity
+ WHERE deleted_at IS NULL
+ AND visibility = 0
+ AND stack_id IS NOT NULL
+ GROUP BY stack_id
+ ) AS stack_count ON rae.stack_id = stack_count.stack_id
WHERE
- rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
+ rae.deleted_at IS NULL
+ AND rae.visibility = 0
+ AND rae.owner_id in ?
+ AND (
+ rae.stack_id IS NULL
+ OR rae.id = se.primary_asset_id
+ )
UNION ALL
SELECT
NULL as remote_id,
@@ -41,7 +62,9 @@ mergedAsset: SELECT * FROM
lae.checksum,
NULL as owner_id,
NULL as live_photo_video_id,
- lae.orientation
+ lae.orientation,
+ NULL as stack_id,
+ 0 AS stack_count
FROM
local_asset_entity lae
LEFT JOIN
@@ -68,8 +91,16 @@ FROM
remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
+ LEFT JOIN
+ stack_entity se ON rae.stack_id = se.id
WHERE
- rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
+ rae.deleted_at IS NULL
+ AND rae.visibility = 0
+ AND rae.owner_id in ?
+ AND (
+ rae.stack_id IS NULL
+ OR rae.id = se.primary_asset_id
+ )
UNION ALL
SELECT
lae.name,
diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart
index 4ee0643706..ac3db868e1 100644
--- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart
+++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart
@@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i3;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i4;
+import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
+ as i5;
class MergedAssetDrift extends i1.ModularAccessor {
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
@@ -18,7 +20,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
- 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
+ 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, COALESCE(stack_count.total_count, 0) AS stack_count FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum LEFT JOIN stack_entity AS se ON rae.stack_id = se.id LEFT JOIN (SELECT stack_id, COUNT(*) AS total_count FROM remote_asset_entity WHERE deleted_at IS NULL AND visibility = 0 AND stack_id IS NOT NULL GROUP BY stack_id) AS stack_count ON rae.stack_id = stack_count.stack_id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, 0 AS stack_count FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in var1) i0.Variable($),
...generatedlimit.introducedVariables
@@ -26,6 +28,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
readsFrom: {
remoteAssetEntity,
localAssetEntity,
+ stackEntity,
...generatedlimit.watchedTables,
}).map((i0.QueryRow row) => MergedAssetResult(
remoteId: row.readNullable('remote_id'),
@@ -44,6 +47,8 @@ class MergedAssetDrift extends i1.ModularAccessor {
ownerId: row.readNullable('owner_id'),
livePhotoVideoId: row.readNullable('live_photo_video_id'),
orientation: row.read('orientation'),
+ stackId: row.readNullable('stack_id'),
+ stackCount: row.read('stack_count'),
));
}
@@ -53,7 +58,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
$arrayStartIndex += var2.length;
return customSelect(
- 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
+ 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
variables: [
i0.Variable(groupBy),
for (var $ in var2) i0.Variable($)
@@ -61,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
readsFrom: {
remoteAssetEntity,
localAssetEntity,
+ stackEntity,
}).map((i0.QueryRow row) => MergedBucketResult(
assetCount: row.read('asset_count'),
bucketDate: row.read('bucket_date'),
@@ -73,6 +79,9 @@ class MergedAssetDrift extends i1.ModularAccessor {
i4.$LocalAssetEntityTable get localAssetEntity =>
i1.ReadDatabaseContainer(attachedDatabase)
.resultSet('local_asset_entity');
+ i5.$StackEntityTable get stackEntity =>
+ i1.ReadDatabaseContainer(attachedDatabase)
+ .resultSet('stack_entity');
}
class MergedAssetResult {
@@ -91,6 +100,8 @@ class MergedAssetResult {
final String? ownerId;
final String? livePhotoVideoId;
final int orientation;
+ final String? stackId;
+ final int stackCount;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -107,6 +118,8 @@ class MergedAssetResult {
this.ownerId,
this.livePhotoVideoId,
required this.orientation,
+ this.stackId,
+ required this.stackCount,
});
}
diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart
new file mode 100644
index 0000000000..68dd04cb5f
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/person.entity.dart
@@ -0,0 +1,34 @@
+import 'package:drift/drift.dart';
+import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
+import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
+
+class PersonEntity extends Table with DriftDefaultsMixin {
+ const PersonEntity();
+
+ TextColumn get id => text()();
+
+ DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
+
+ DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
+
+ TextColumn get ownerId =>
+ text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
+
+ TextColumn get name => text()();
+
+ // TODO: foreign key refering to asset faces
+ TextColumn get faceAssetId => text().nullable()();
+
+ TextColumn get thumbnailPath => text()();
+
+ BoolColumn get isFavorite => boolean()();
+
+ BoolColumn get isHidden => boolean()();
+
+ TextColumn get color => text().nullable()();
+
+ DateTimeColumn get birthDate => dateTime().nullable()();
+
+ @override
+ Set get primaryKey => {id};
+}
diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart
new file mode 100644
index 0000000000..f0ced63f0e
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/person.entity.drift.dart
@@ -0,0 +1,933 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
+ as i1;
+import 'package:immich_mobile/infrastructure/entities/person.entity.dart' as i2;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
+import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
+ as i4;
+import 'package:drift/internal/modular.dart' as i5;
+
+typedef $$PersonEntityTableCreateCompanionBuilder = i1.PersonEntityCompanion
+ Function({
+ required String id,
+ i0.Value createdAt,
+ i0.Value updatedAt,
+ required String ownerId,
+ required String name,
+ i0.Value faceAssetId,
+ required String thumbnailPath,
+ required bool isFavorite,
+ required bool isHidden,
+ i0.Value color,
+ i0.Value birthDate,
+});
+typedef $$PersonEntityTableUpdateCompanionBuilder = i1.PersonEntityCompanion
+ Function({
+ i0.Value id,
+ i0.Value createdAt,
+ i0.Value updatedAt,
+ i0.Value ownerId,
+ i0.Value name,
+ i0.Value faceAssetId,
+ i0.Value thumbnailPath,
+ i0.Value isFavorite,
+ i0.Value isHidden,
+ i0.Value color,
+ i0.Value 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('user_entity')
+ .createAlias(i0.$_aliasNameGenerator(
+ i5.ReadDatabaseContainer(db)
+ .resultSet('person_entity')
+ .ownerId,
+ i5.ReadDatabaseContainer(db)
+ .resultSet('user_entity')
+ .id));
+
+ i4.$$UserEntityTableProcessedTableManager get ownerId {
+ final $_column = $_itemColumn('owner_id')!;
+
+ final manager = i4
+ .$$UserEntityTableTableManager(
+ $_db,
+ i5.ReadDatabaseContainer($_db)
+ .resultSet('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 {
+ $$PersonEntityTableFilterComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.ColumnFilters get id => $composableBuilder(
+ column: $table.id, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get createdAt => $composableBuilder(
+ column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get updatedAt => $composableBuilder(
+ column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get name => $composableBuilder(
+ column: $table.name, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get faceAssetId => $composableBuilder(
+ column: $table.faceAssetId,
+ builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get thumbnailPath => $composableBuilder(
+ column: $table.thumbnailPath,
+ builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get isFavorite => $composableBuilder(
+ column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get isHidden => $composableBuilder(
+ column: $table.isHidden, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters get color => $composableBuilder(
+ column: $table.color, builder: (column) => i0.ColumnFilters(column));
+
+ i0.ColumnFilters 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('user_entity'),
+ getReferencedColumn: (t) => t.id,
+ builder: (joinBuilder,
+ {$addJoinBuilderToRootComposer,
+ $removeJoinBuilderFromRootComposer}) =>
+ i4.$$UserEntityTableFilterComposer(
+ $db: $db,
+ $table: i5.ReadDatabaseContainer($db)
+ .resultSet('user_entity'),
+ $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+ joinBuilder: joinBuilder,
+ $removeJoinBuilderFromRootComposer:
+ $removeJoinBuilderFromRootComposer,
+ ));
+ return composer;
+ }
+}
+
+class $$PersonEntityTableOrderingComposer
+ extends i0.Composer {
+ $$PersonEntityTableOrderingComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.ColumnOrderings get id => $composableBuilder(
+ column: $table.id, builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get createdAt => $composableBuilder(
+ column: $table.createdAt,
+ builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get updatedAt => $composableBuilder(
+ column: $table.updatedAt,
+ builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get name => $composableBuilder(
+ column: $table.name, builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get faceAssetId => $composableBuilder(
+ column: $table.faceAssetId,
+ builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get thumbnailPath => $composableBuilder(
+ column: $table.thumbnailPath,
+ builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get isFavorite => $composableBuilder(
+ column: $table.isFavorite,
+ builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get isHidden => $composableBuilder(
+ column: $table.isHidden, builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings get color => $composableBuilder(
+ column: $table.color, builder: (column) => i0.ColumnOrderings(column));
+
+ i0.ColumnOrderings 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('user_entity'),
+ getReferencedColumn: (t) => t.id,
+ builder: (joinBuilder,
+ {$addJoinBuilderToRootComposer,
+ $removeJoinBuilderFromRootComposer}) =>
+ i4.$$UserEntityTableOrderingComposer(
+ $db: $db,
+ $table: i5.ReadDatabaseContainer($db)
+ .resultSet('user_entity'),
+ $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+ joinBuilder: joinBuilder,
+ $removeJoinBuilderFromRootComposer:
+ $removeJoinBuilderFromRootComposer,
+ ));
+ return composer;
+ }
+}
+
+class $$PersonEntityTableAnnotationComposer
+ extends i0.Composer {
+ $$PersonEntityTableAnnotationComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.GeneratedColumn get id =>
+ $composableBuilder(column: $table.id, builder: (column) => column);
+
+ i0.GeneratedColumn get createdAt =>
+ $composableBuilder(column: $table.createdAt, builder: (column) => column);
+
+ i0.GeneratedColumn get updatedAt =>
+ $composableBuilder(column: $table.updatedAt, builder: (column) => column);
+
+ i0.GeneratedColumn get name =>
+ $composableBuilder(column: $table.name, builder: (column) => column);
+
+ i0.GeneratedColumn get faceAssetId => $composableBuilder(
+ column: $table.faceAssetId, builder: (column) => column);
+
+ i0.GeneratedColumn get thumbnailPath => $composableBuilder(
+ column: $table.thumbnailPath, builder: (column) => column);
+
+ i0.GeneratedColumn get isFavorite => $composableBuilder(
+ column: $table.isFavorite, builder: (column) => column);
+
+ i0.GeneratedColumn get isHidden =>
+ $composableBuilder(column: $table.isHidden, builder: (column) => column);
+
+ i0.GeneratedColumn get color =>
+ $composableBuilder(column: $table.color, builder: (column) => column);
+
+ i0.GeneratedColumn 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('user_entity'),
+ getReferencedColumn: (t) => t.id,
+ builder: (joinBuilder,
+ {$addJoinBuilderToRootComposer,
+ $removeJoinBuilderFromRootComposer}) =>
+ i4.$$UserEntityTableAnnotationComposer(
+ $db: $db,
+ $table: i5.ReadDatabaseContainer($db)
+ .resultSet('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 id = const i0.Value.absent(),
+ i0.Value createdAt = const i0.Value.absent(),
+ i0.Value updatedAt = const i0.Value.absent(),
+ i0.Value ownerId = const i0.Value.absent(),
+ i0.Value name = const i0.Value.absent(),
+ i0.Value faceAssetId = const i0.Value.absent(),
+ i0.Value thumbnailPath = const i0.Value.absent(),
+ i0.Value isFavorite = const i0.Value.absent(),
+ i0.Value isHidden = const i0.Value.absent(),
+ i0.Value color = const i0.Value.absent(),
+ i0.Value 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 createdAt = const i0.Value.absent(),
+ i0.Value updatedAt = const i0.Value.absent(),
+ required String ownerId,
+ required String name,
+ i0.Value faceAssetId = const i0.Value.absent(),
+ required String thumbnailPath,
+ required bool isFavorite,
+ required bool isHidden,
+ i0.Value color = const i0.Value.absent(),
+ i0.Value 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 id = i0.GeneratedColumn(
+ 'id', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _createdAtMeta =
+ const i0.VerificationMeta('createdAt');
+ @override
+ late final i0.GeneratedColumn createdAt =
+ i0.GeneratedColumn('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 updatedAt =
+ i0.GeneratedColumn('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 ownerId = i0.GeneratedColumn(
+ '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 name = i0.GeneratedColumn(
+ 'name', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _faceAssetIdMeta =
+ const i0.VerificationMeta('faceAssetId');
+ @override
+ late final i0.GeneratedColumn faceAssetId =
+ i0.GeneratedColumn('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 thumbnailPath =
+ i0.GeneratedColumn('thumbnail_path', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _isFavoriteMeta =
+ const i0.VerificationMeta('isFavorite');
+ @override
+ late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn(
+ '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 isHidden = i0.GeneratedColumn(
+ '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 color = i0.GeneratedColumn(
+ 'color', aliasedName, true,
+ type: i0.DriftSqlType.string, requiredDuringInsert: false);
+ static const i0.VerificationMeta _birthDateMeta =
+ const i0.VerificationMeta('birthDate');
+ @override
+ late final i0.GeneratedColumn birthDate =
+ i0.GeneratedColumn('birth_date', aliasedName, true,
+ type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
+ @override
+ List 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 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 get $primaryKey => {id};
+ @override
+ i1.PersonEntityData map(Map 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 {
+ 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 toColumns(bool nullToAbsent) {
+ final map = {};
+ map['id'] = i0.Variable(id);
+ map['created_at'] = i0.Variable(createdAt);
+ map['updated_at'] = i0.Variable(updatedAt);
+ map['owner_id'] = i0.Variable(ownerId);
+ map['name'] = i0.Variable(name);
+ if (!nullToAbsent || faceAssetId != null) {
+ map['face_asset_id'] = i0.Variable(faceAssetId);
+ }
+ map['thumbnail_path'] = i0.Variable(thumbnailPath);
+ map['is_favorite'] = i0.Variable(isFavorite);
+ map['is_hidden'] = i0.Variable(isHidden);
+ if (!nullToAbsent || color != null) {
+ map['color'] = i0.Variable