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. -[![Open in VSCode Containers](https://img.shields.io/static/v1?label=VSCode%20DevContainer&message=Immich&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/) +Get started fast! [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = i0.Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + i1.PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + i0.Value faceAssetId = const i0.Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + i0.Value color = const i0.Value.absent(), + i0.Value birthDate = const i0.Value.absent()}) => + i1.PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(i1.PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: + data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present + ? data.thumbnailPath.value + : this.thumbnailPath, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name, + faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value name; + final i0.Value faceAssetId; + final i0.Value thumbnailPath; + final i0.Value isFavorite; + final i0.Value isHidden; + final i0.Value color; + final i0.Value birthDate; + const PersonEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.faceAssetId = const i0.Value.absent(), + this.thumbnailPath = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + this.isHidden = const i0.Value.absent(), + this.color = const i0.Value.absent(), + this.birthDate = const i0.Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const i0.Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const i0.Value.absent(), + this.birthDate = const i0.Value.absent(), + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + name = i0.Value(name), + thumbnailPath = i0.Value(thumbnailPath), + isFavorite = i0.Value(isFavorite), + isHidden = i0.Value(isHidden); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? name, + i0.Expression? faceAssetId, + i0.Expression? thumbnailPath, + i0.Expression? isFavorite, + i0.Expression? isHidden, + i0.Expression? color, + i0.Expression? birthDate, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + i1.PersonEntityCompanion copyWith( + {i0.Value? 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}) { + return i1.PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = i0.Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = i0.Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = i0.Variable(isHidden.value); + } + if (color.present) { + map['color'] = i0.Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = i0.Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 96193e0415..0b2896538e 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -34,6 +34,8 @@ class RemoteAssetEntity extends Table IntColumn get visibility => intEnum()(); + TextColumn get stackId => text().nullable()(); + @override Set get primaryKey => {id}; } @@ -55,5 +57,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { visibility: visibility, livePhotoVideoId: livePhotoVideoId, localId: null, + stackId: stackId, ); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index 2bb7cffe59..543ed65985 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -29,6 +29,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder i0.Value deletedAt, i0.Value livePhotoVideoId, required i2.AssetVisibility visibility, + i0.Value stackId, }); typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i1.RemoteAssetEntityCompanion Function({ @@ -48,6 +49,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder i0.Value deletedAt, i0.Value livePhotoVideoId, i0.Value visibility, + i0.Value stackId, }); final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< @@ -145,6 +147,9 @@ class $$RemoteAssetEntityTableFilterComposer column: $table.visibility, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + i0.ColumnFilters get stackId => $composableBuilder( + column: $table.stackId, builder: (column) => i0.ColumnFilters(column)); + i5.$$UserEntityTableFilterComposer get ownerId { final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -231,6 +236,9 @@ class $$RemoteAssetEntityTableOrderingComposer column: $table.visibility, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get stackId => $composableBuilder( + column: $table.stackId, builder: (column) => i0.ColumnOrderings(column)); + i5.$$UserEntityTableOrderingComposer get ownerId { final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, @@ -309,6 +317,9 @@ class $$RemoteAssetEntityTableAnnotationComposer $composableBuilder( column: $table.visibility, builder: (column) => column); + i0.GeneratedColumn get stackId => + $composableBuilder(column: $table.stackId, builder: (column) => column); + i5.$$UserEntityTableAnnotationComposer get ownerId { final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -373,6 +384,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value deletedAt = const i0.Value.absent(), i0.Value livePhotoVideoId = const i0.Value.absent(), i0.Value visibility = const i0.Value.absent(), + i0.Value stackId = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion( name: name, @@ -391,6 +403,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< deletedAt: deletedAt, livePhotoVideoId: livePhotoVideoId, visibility: visibility, + stackId: stackId, ), createCompanionCallback: ({ required String name, @@ -409,6 +422,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value deletedAt = const i0.Value.absent(), i0.Value livePhotoVideoId = const i0.Value.absent(), required i2.AssetVisibility visibility, + i0.Value stackId = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion.insert( name: name, @@ -427,6 +441,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< deletedAt: deletedAt, livePhotoVideoId: livePhotoVideoId, visibility: visibility, + stackId: stackId, ), withReferenceMapper: (p0) => p0 .map((e) => ( @@ -602,6 +617,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity type: i0.DriftSqlType.int, requiredDuringInsert: true) .withConverter( i1.$RemoteAssetEntityTable.$convertervisibility); + static const i0.VerificationMeta _stackIdMeta = + const i0.VerificationMeta('stackId'); + @override + late final i0.GeneratedColumn stackId = i0.GeneratedColumn( + 'stack_id', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); @override List get $columns => [ name, @@ -619,7 +640,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity thumbHash, deletedAt, livePhotoVideoId, - visibility + visibility, + stackId ]; @override String get aliasedName => _alias ?? actualTableName; @@ -703,6 +725,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity livePhotoVideoId.isAcceptableOrUnknown( data['live_photo_video_id']!, _livePhotoVideoIdMeta)); } + if (data.containsKey('stack_id')) { + context.handle(_stackIdMeta, + stackId.isAcceptableOrUnknown(data['stack_id']!, _stackIdMeta)); + } return context; } @@ -748,6 +774,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), + stackId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}stack_id']), ); } @@ -785,6 +813,7 @@ class RemoteAssetEntityData extends i0.DataClass final DateTime? deletedAt; final String? livePhotoVideoId; final i2.AssetVisibility visibility; + final String? stackId; const RemoteAssetEntityData( {required this.name, required this.type, @@ -801,7 +830,8 @@ class RemoteAssetEntityData extends i0.DataClass this.thumbHash, this.deletedAt, this.livePhotoVideoId, - required this.visibility}); + required this.visibility, + this.stackId}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -841,6 +871,9 @@ class RemoteAssetEntityData extends i0.DataClass map['visibility'] = i0.Variable( i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); } + if (!nullToAbsent || stackId != null) { + map['stack_id'] = i0.Variable(stackId); + } return map; } @@ -866,6 +899,7 @@ class RemoteAssetEntityData extends i0.DataClass livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), visibility: i1.$RemoteAssetEntityTable.$convertervisibility .fromJson(serializer.fromJson(json['visibility'])), + stackId: serializer.fromJson(json['stackId']), ); } @override @@ -890,6 +924,7 @@ class RemoteAssetEntityData extends i0.DataClass 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), 'visibility': serializer.toJson( i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), + 'stackId': serializer.toJson(stackId), }; } @@ -909,7 +944,8 @@ class RemoteAssetEntityData extends i0.DataClass i0.Value thumbHash = const i0.Value.absent(), i0.Value deletedAt = const i0.Value.absent(), i0.Value livePhotoVideoId = const i0.Value.absent(), - i2.AssetVisibility? visibility}) => + i2.AssetVisibility? visibility, + i0.Value stackId = const i0.Value.absent()}) => i1.RemoteAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -932,6 +968,7 @@ class RemoteAssetEntityData extends i0.DataClass ? livePhotoVideoId.value : this.livePhotoVideoId, visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, ); RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { return RemoteAssetEntityData( @@ -959,6 +996,7 @@ class RemoteAssetEntityData extends i0.DataClass : this.livePhotoVideoId, visibility: data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, ); } @@ -980,7 +1018,8 @@ class RemoteAssetEntityData extends i0.DataClass ..write('thumbHash: $thumbHash, ') ..write('deletedAt: $deletedAt, ') ..write('livePhotoVideoId: $livePhotoVideoId, ') - ..write('visibility: $visibility') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') ..write(')')) .toString(); } @@ -1002,7 +1041,8 @@ class RemoteAssetEntityData extends i0.DataClass thumbHash, deletedAt, livePhotoVideoId, - visibility); + visibility, + stackId); @override bool operator ==(Object other) => identical(this, other) || @@ -1022,7 +1062,8 @@ class RemoteAssetEntityData extends i0.DataClass other.thumbHash == this.thumbHash && other.deletedAt == this.deletedAt && other.livePhotoVideoId == this.livePhotoVideoId && - other.visibility == this.visibility); + other.visibility == this.visibility && + other.stackId == this.stackId); } class RemoteAssetEntityCompanion @@ -1043,6 +1084,7 @@ class RemoteAssetEntityCompanion final i0.Value deletedAt; final i0.Value livePhotoVideoId; final i0.Value visibility; + final i0.Value stackId; const RemoteAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -1060,6 +1102,7 @@ class RemoteAssetEntityCompanion this.deletedAt = const i0.Value.absent(), this.livePhotoVideoId = const i0.Value.absent(), this.visibility = const i0.Value.absent(), + this.stackId = const i0.Value.absent(), }); RemoteAssetEntityCompanion.insert({ required String name, @@ -1078,6 +1121,7 @@ class RemoteAssetEntityCompanion this.deletedAt = const i0.Value.absent(), this.livePhotoVideoId = const i0.Value.absent(), required i2.AssetVisibility visibility, + this.stackId = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id), @@ -1101,6 +1145,7 @@ class RemoteAssetEntityCompanion i0.Expression? deletedAt, i0.Expression? livePhotoVideoId, i0.Expression? visibility, + i0.Expression? stackId, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -1119,6 +1164,7 @@ class RemoteAssetEntityCompanion if (deletedAt != null) 'deleted_at': deletedAt, if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, }); } @@ -1138,7 +1184,8 @@ class RemoteAssetEntityCompanion i0.Value? thumbHash, i0.Value? deletedAt, i0.Value? livePhotoVideoId, - i0.Value? visibility}) { + i0.Value? visibility, + i0.Value? stackId}) { return i1.RemoteAssetEntityCompanion( name: name ?? this.name, type: type ?? this.type, @@ -1156,6 +1203,7 @@ class RemoteAssetEntityCompanion deletedAt: deletedAt ?? this.deletedAt, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, ); } @@ -1213,6 +1261,9 @@ class RemoteAssetEntityCompanion .$RemoteAssetEntityTable.$convertervisibility .toSql(visibility.value)); } + if (stackId.present) { + map['stack_id'] = i0.Variable(stackId.value); + } return map; } @@ -1234,7 +1285,8 @@ class RemoteAssetEntityCompanion ..write('thumbHash: $thumbHash, ') ..write('deletedAt: $deletedAt, ') ..write('livePhotoVideoId: $livePhotoVideoId, ') - ..write('visibility: $visibility') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index 92375f19db..b5da42832e 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -1,5 +1,4 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @@ -15,7 +14,7 @@ class StackEntity extends Table with DriftDefaultsMixin { TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)(); + TextColumn get primaryAssetId => text()(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart index c0d000e02a..df0390aea0 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.drift.dart @@ -8,8 +8,6 @@ import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i4; import 'package:drift/internal/modular.dart' as i5; -import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' - as i6; typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion Function({ @@ -57,33 +55,6 @@ final class $$StackEntityTableReferences extends i0.BaseReferences< return i0.ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } - - static i6.$RemoteAssetEntityTable _primaryAssetIdTable( - i0.GeneratedDatabase db) => - i5.ReadDatabaseContainer(db) - .resultSet('remote_asset_entity') - .createAlias(i0.$_aliasNameGenerator( - i5.ReadDatabaseContainer(db) - .resultSet('stack_entity') - .primaryAssetId, - i5.ReadDatabaseContainer(db) - .resultSet('remote_asset_entity') - .id)); - - i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId { - final $_column = $_itemColumn('primary_asset_id')!; - - final manager = i6 - .$$RemoteAssetEntityTableTableManager( - $_db, - i5.ReadDatabaseContainer($_db) - .resultSet('remote_asset_entity')) - .filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db)); - if (item == null) return manager; - return i0.ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } } class $$StackEntityTableFilterComposer @@ -104,6 +75,10 @@ class $$StackEntityTableFilterComposer i0.ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, + builder: (column) => i0.ColumnFilters(column)); + i4.$$UserEntityTableFilterComposer get ownerId { final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -125,28 +100,6 @@ class $$StackEntityTableFilterComposer )); return composer; } - - i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId { - final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.primaryAssetId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - i6.$$RemoteAssetEntityTableFilterComposer( - $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet('remote_asset_entity'), - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$StackEntityTableOrderingComposer @@ -169,6 +122,10 @@ class $$StackEntityTableOrderingComposer column: $table.updatedAt, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, + builder: (column) => i0.ColumnOrderings(column)); + i4.$$UserEntityTableOrderingComposer get ownerId { final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, @@ -190,30 +147,6 @@ class $$StackEntityTableOrderingComposer )); return composer; } - - i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId { - final i6.$$RemoteAssetEntityTableOrderingComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.primaryAssetId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - i6.$$RemoteAssetEntityTableOrderingComposer( - $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet( - 'remote_asset_entity'), - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$StackEntityTableAnnotationComposer @@ -234,6 +167,9 @@ class $$StackEntityTableAnnotationComposer i0.GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + i0.GeneratedColumn get primaryAssetId => $composableBuilder( + column: $table.primaryAssetId, builder: (column) => column); + i4.$$UserEntityTableAnnotationComposer get ownerId { final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -255,30 +191,6 @@ class $$StackEntityTableAnnotationComposer )); return composer; } - - i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId { - final i6.$$RemoteAssetEntityTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.primaryAssetId, - referencedTable: i5.ReadDatabaseContainer($db) - .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - i6.$$RemoteAssetEntityTableAnnotationComposer( - $db: $db, - $table: i5.ReadDatabaseContainer($db) - .resultSet( - 'remote_asset_entity'), - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } } class $$StackEntityTableTableManager extends i0.RootTableManager< @@ -292,7 +204,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager< $$StackEntityTableUpdateCompanionBuilder, (i1.StackEntityData, i1.$$StackEntityTableReferences), i1.StackEntityData, - i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> { + i0.PrefetchHooks Function({bool ownerId})> { $$StackEntityTableTableManager( i0.GeneratedDatabase db, i1.$StackEntityTable table) : super(i0.TableManagerState( @@ -338,7 +250,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager< i1.$$StackEntityTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) { + prefetchHooksCallback: ({ownerId = false}) { return i0.PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -365,17 +277,6 @@ class $$StackEntityTableTableManager extends i0.RootTableManager< i1.$$StackEntityTableReferences._ownerIdTable(db).id, ) as T; } - if (primaryAssetId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.primaryAssetId, - referencedTable: i1.$$StackEntityTableReferences - ._primaryAssetIdTable(db), - referencedColumn: i1.$$StackEntityTableReferences - ._primaryAssetIdTable(db) - .id, - ) as T; - } return state; }, @@ -398,7 +299,7 @@ typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager< $$StackEntityTableUpdateCompanionBuilder, (i1.StackEntityData, i1.$$StackEntityTableReferences), i1.StackEntityData, - i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>; + i0.PrefetchHooks Function({bool ownerId})>; class $StackEntityTable extends i2.StackEntity with i0.TableInfo<$StackEntityTable, i1.StackEntityData> { @@ -440,12 +341,8 @@ class $StackEntityTable extends i2.StackEntity const i0.VerificationMeta('primaryAssetId'); @override late final i0.GeneratedColumn primaryAssetId = - i0.GeneratedColumn( - 'primary_asset_id', aliasedName, false, - type: i0.DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES remote_asset_entity (id)')); + i0.GeneratedColumn('primary_asset_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); @override List get $columns => [id, createdAt, updatedAt, ownerId, primaryAssetId]; diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart new file mode 100644 index 0000000000..99df206db5 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -0,0 +1,157 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import "package:immich_mobile/utils/database.utils.dart"; + +final backupRepositoryProvider = Provider( + (ref) => DriftBackupRepository(ref.watch(driftProvider)), +); + +class DriftBackupRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftBackupRepository(this._db) : super(_db); + + _getExcludedSubquery() { + return _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection + .equalsValue(BackupSelection.excluded), + ); + } + + Future getTotalCount() async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection + .equalsValue(BackupSelection.selected) & + _db.localAlbumAssetEntity.assetId + .isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future getRemainderCount() async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum + .equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection + .equalsValue(BackupSelection.selected) & + _db.remoteAssetEntity.id.isNull() & + _db.localAlbumAssetEntity.assetId + .isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future getBackupCount() async { + final query = _db.localAlbumAssetEntity.selectOnly(distinct: true) + ..addColumns( + [_db.localAlbumAssetEntity.assetId], + ) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + innerJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum + .equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where( + _db.localAlbumEntity.backupSelection + .equalsValue(BackupSelection.selected) & + _db.remoteAssetEntity.id.isNotNull() & + _db.localAlbumAssetEntity.assetId + .isNotInQuery(_getExcludedSubquery()), + ); + + return query.get().then((rows) => rows.length); + } + + Future> getCandidates() async { + final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true) + ..addColumns([_db.localAlbumEntity.id]) + ..where( + _db.localAlbumEntity.backupSelection + .equalsValue(BackupSelection.selected), + ); + + final query = _db.localAssetEntity.select() + ..where( + (lae) => + existsQuery( + _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where( + _db.localAlbumAssetEntity.albumId + .isInQuery(selectedAlbumIds) & + _db.localAlbumAssetEntity.assetId.equalsExp(lae.id), + ), + ) & + notExistsQuery( + _db.remoteAssetEntity.selectOnly() + ..addColumns([_db.remoteAssetEntity.checksum]) + ..where( + _db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & + lae.checksum.isNotNull(), + ), + ) & + lae.id.isNotInQuery(_getExcludedSubquery()), + ) + ..orderBy( + [ + (localAsset) => OrderingTerm.desc(localAsset.createdAt), + ], + ); + + return query.map((localAsset) => localAsset.toDto()).get(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index a7920cf7b2..7562cf6ff5 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; @@ -10,6 +11,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; @@ -17,6 +19,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; import 'package:isar/isar.dart'; import 'db.repository.drift.dart'; @@ -52,6 +55,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { MemoryEntity, MemoryAssetEntity, StackEntity, + PersonEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', @@ -68,10 +72,40 @@ class Drift extends $Drift implements IDatabaseRepository { ); @override - int get schemaVersion => 1; + int get schemaVersion => 3; @override MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + // Run migration steps without foreign keys and re-enable them later + await customStatement('PRAGMA foreign_keys = OFF'); + + await m.runMigrationSteps( + from: from, + to: to, + steps: migrationSteps( + from1To2: (m, v2) async { + for (final entity in v2.entities) { + await m.drop(entity); + await m.create(entity); + } + }, + from2To3: (m, v3) async { + // Removed foreign key constraint on stack.primaryAssetId + await m.alterTable(TableMigration(v3.stackEntity)); + }, + ), + ); + + if (kDebugMode) { + // Fail if the migration broke foreign keys + final wrongFKs = + await customSelect('PRAGMA foreign_key_check').get(); + assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}'); + } + + await customStatement('PRAGMA foreign_keys = ON;'); + }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA synchronous = NORMAL'); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 15d445d226..0f822e57eb 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -7,31 +7,33 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift. as i2; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' as i3; -import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' - as i4; -import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' - as i5; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' - as i6; -import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' - as i7; -import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' - as i8; -import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' - as i9; -import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' - as i10; -import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' - as i11; -import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' - as i12; -import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' - as i13; import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i7; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i8; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i9; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i10; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' + as i11; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' + as i12; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i13; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' as i14; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' as i15; -import 'package:drift/internal/modular.dart' as i16; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i16; +import 'package:drift/internal/modular.dart' as i17; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -41,28 +43,29 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.$RemoteAssetEntityTable(this); late final i3.$LocalAssetEntityTable localAssetEntity = i3.$LocalAssetEntityTable(this); - late final i4.$UserMetadataEntityTable userMetadataEntity = - i4.$UserMetadataEntityTable(this); - late final i5.$PartnerEntityTable partnerEntity = - i5.$PartnerEntityTable(this); - late final i6.$LocalAlbumEntityTable localAlbumEntity = - i6.$LocalAlbumEntityTable(this); - late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = - i7.$LocalAlbumAssetEntityTable(this); - late final i8.$RemoteExifEntityTable remoteExifEntity = - i8.$RemoteExifEntityTable(this); - late final i9.$RemoteAlbumEntityTable remoteAlbumEntity = - i9.$RemoteAlbumEntityTable(this); - late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = - i10.$RemoteAlbumAssetEntityTable(this); - late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = - i11.$RemoteAlbumUserEntityTable(this); - late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this); - late final i13.$MemoryAssetEntityTable memoryAssetEntity = - i13.$MemoryAssetEntityTable(this); - late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this); - i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this) - .accessor(i15.MergedAssetDrift.new); + late final i4.$StackEntityTable stackEntity = i4.$StackEntityTable(this); + late final i5.$UserMetadataEntityTable userMetadataEntity = + i5.$UserMetadataEntityTable(this); + late final i6.$PartnerEntityTable partnerEntity = + i6.$PartnerEntityTable(this); + late final i7.$LocalAlbumEntityTable localAlbumEntity = + i7.$LocalAlbumEntityTable(this); + late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i8.$LocalAlbumAssetEntityTable(this); + late final i9.$RemoteExifEntityTable remoteExifEntity = + i9.$RemoteExifEntityTable(this); + late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = + i10.$RemoteAlbumEntityTable(this); + late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = + i11.$RemoteAlbumAssetEntityTable(this); + late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = + i12.$RemoteAlbumUserEntityTable(this); + late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this); + late final i14.$MemoryAssetEntityTable memoryAssetEntity = + i14.$MemoryAssetEntityTable(this); + late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this); + i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this) + .accessor(i16.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -71,6 +74,7 @@ abstract class $Drift extends i0.GeneratedDatabase { userEntity, remoteAssetEntity, localAssetEntity, + stackEntity, i3.idxLocalAssetChecksum, i2.uQRemoteAssetOwnerChecksum, i2.idxRemoteAssetChecksum, @@ -84,7 +88,7 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteAlbumUserEntity, memoryEntity, memoryAssetEntity, - stackEntity + personEntity ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -97,6 +101,13 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete), + ], + ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName('user_entity', limitUpdateKind: i0.UpdateKind.delete), @@ -213,7 +224,7 @@ abstract class $Drift extends i0.GeneratedDatabase { on: i0.TableUpdateQuery.onTableName('user_entity', limitUpdateKind: i0.UpdateKind.delete), result: [ - i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete), + i0.TableUpdate('person_entity', kind: i0.UpdateKind.delete), ], ), ], @@ -232,27 +243,29 @@ class $DriftManager { i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); i3.$$LocalAssetEntityTableTableManager get localAssetEntity => i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); - i4.$$UserMetadataEntityTableTableManager get userMetadataEntity => - i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); - i5.$$PartnerEntityTableTableManager get partnerEntity => - i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); - i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity => - i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); - i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7 + i4.$$StackEntityTableTableManager get stackEntity => + i4.$$StackEntityTableTableManager(_db, _db.stackEntity); + i5.$$UserMetadataEntityTableTableManager get userMetadataEntity => + i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); + i6.$$PartnerEntityTableTableManager get partnerEntity => + i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8 .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); - i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => - i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); - i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => - i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); - i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => - i10.$$RemoteAlbumAssetEntityTableTableManager( + i9.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); + i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => + i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); + i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => + i11.$$RemoteAlbumAssetEntityTableTableManager( _db, _db.remoteAlbumAssetEntity); - i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11 + i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12 .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); - i12.$$MemoryEntityTableTableManager get memoryEntity => - i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); - i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => - i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); - i14.$$StackEntityTableTableManager get stackEntity => - i14.$$StackEntityTableTableManager(_db, _db.stackEntity); + i13.$$MemoryEntityTableTableManager get memoryEntity => + i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); + i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => + i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); + i15.$$PersonEntityTableTableManager get personEntity => + i15.$$PersonEntityTableTableManager(_db, _db.personEntity); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart new file mode 100644 index 0000000000..a0703c3714 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -0,0 +1,1299 @@ +// dart format width=80 +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'dart:typed_data' as i2; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class Schema2 extends i0.VersionedSchema { + Schema2({required super.database}) : super(version: 2); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + ]; + late final Shape0 userEntity = Shape0( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 remoteAssetEntity = Shape1( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_24, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + final i1.Index uQRemoteAssetOwnerChecksum = i1.Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum', + 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(user_id, "key")', + ], + columns: [ + _column_25, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 localAlbumEntity = Shape6( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_36, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(album_id, user_id)', + ], + columns: [ + _column_60, + _column_25, + _column_61, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, memory_id)', + ], + columns: [ + _column_36, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 personEntity = Shape13( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_70, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get isAdmin => + columnsByName['is_admin']! as i1.GeneratedColumn; + i1.GeneratedColumn get email => + columnsByName['email']! as i1.GeneratedColumn; + i1.GeneratedColumn get profileImagePath => + columnsByName['profile_image_path']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaSizeInBytes => + columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn; + i1.GeneratedColumn get quotaUsageInBytes => + columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn('name', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_2(String aliasedName) => + i1.GeneratedColumn('is_admin', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn('email', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn('profile_image_path', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn('updated_at', aliasedName, false, + type: i1.DriftSqlType.dateTime, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); +i1.GeneratedColumn _column_6(String aliasedName) => + i1.GeneratedColumn('quota_size_in_bytes', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_7(String aliasedName) => + i1.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, + type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0')); + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => + columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => + columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get durationInSeconds => + columnsByName['duration_in_seconds']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get checksum => + columnsByName['checksum']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get localDateTime => + columnsByName['local_date_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbHash => + columnsByName['thumb_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get livePhotoVideoId => + columnsByName['live_photo_video_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get visibility => + columnsByName['visibility']! as i1.GeneratedColumn; + i1.GeneratedColumn get stackId => + columnsByName['stack_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_8(String aliasedName) => + i1.GeneratedColumn('type', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_9(String aliasedName) => + i1.GeneratedColumn('created_at', aliasedName, false, + type: i1.DriftSqlType.dateTime, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); +i1.GeneratedColumn _column_10(String aliasedName) => + i1.GeneratedColumn('width', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_11(String aliasedName) => + i1.GeneratedColumn('height', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_12(String aliasedName) => + i1.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_13(String aliasedName) => + i1.GeneratedColumn('checksum', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_14(String aliasedName) => + i1.GeneratedColumn('is_favorite', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_15(String aliasedName) => + i1.GeneratedColumn('owner_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_16(String aliasedName) => + i1.GeneratedColumn('local_date_time', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_17(String aliasedName) => + i1.GeneratedColumn('thumb_hash', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_18(String aliasedName) => + i1.GeneratedColumn('deleted_at', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_19(String aliasedName) => + i1.GeneratedColumn('live_photo_video_id', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_20(String aliasedName) => + i1.GeneratedColumn('visibility', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_21(String aliasedName) => + i1.GeneratedColumn('stack_id', aliasedName, true, + type: i1.DriftSqlType.string); + +class Shape2 extends i0.VersionedTable { + Shape2({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => + columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => + columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get durationInSeconds => + columnsByName['duration_in_seconds']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get checksum => + columnsByName['checksum']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get orientation => + columnsByName['orientation']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_22(String aliasedName) => + i1.GeneratedColumn('checksum', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_23(String aliasedName) => + i1.GeneratedColumn('orientation', aliasedName, false, + type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0')); + +class Shape3 extends i0.VersionedTable { + Shape3({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get primaryAssetId => + columnsByName['primary_asset_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_24(String aliasedName) => + i1.GeneratedColumn('primary_asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id)')); + +class Shape4 extends i0.VersionedTable { + Shape4({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get userId => + columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get key => + columnsByName['key']! as i1.GeneratedColumn; + i1.GeneratedColumn get value => + columnsByName['value']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_25(String aliasedName) => + i1.GeneratedColumn('user_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_26(String aliasedName) => + i1.GeneratedColumn('key', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_27(String aliasedName) => + i1.GeneratedColumn('value', aliasedName, false, + type: i1.DriftSqlType.blob); + +class Shape5 extends i0.VersionedTable { + Shape5({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get sharedById => + columnsByName['shared_by_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get sharedWithId => + columnsByName['shared_with_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get inTimeline => + columnsByName['in_timeline']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_28(String aliasedName) => + i1.GeneratedColumn('shared_by_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_29(String aliasedName) => + i1.GeneratedColumn('shared_with_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_30(String aliasedName) => + i1.GeneratedColumn('in_timeline', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + +class Shape6 extends i0.VersionedTable { + Shape6({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get backupSelection => + columnsByName['backup_selection']! as i1.GeneratedColumn; + i1.GeneratedColumn get isIosSharedAlbum => + columnsByName['is_ios_shared_album']! as i1.GeneratedColumn; + i1.GeneratedColumn get marker_ => + columnsByName['marker']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_31(String aliasedName) => + i1.GeneratedColumn('backup_selection', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_32(String aliasedName) => + i1.GeneratedColumn('is_ios_shared_album', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_33(String aliasedName) => + i1.GeneratedColumn('marker', aliasedName, true, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))')); + +class Shape7 extends i0.VersionedTable { + Shape7({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumId => + columnsByName['album_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_34(String aliasedName) => + i1.GeneratedColumn('asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_35(String aliasedName) => + i1.GeneratedColumn('album_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + +class Shape8 extends i0.VersionedTable { + Shape8({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get city => + columnsByName['city']! as i1.GeneratedColumn; + i1.GeneratedColumn get state => + columnsByName['state']! as i1.GeneratedColumn; + i1.GeneratedColumn get country => + columnsByName['country']! as i1.GeneratedColumn; + i1.GeneratedColumn get dateTimeOriginal => + columnsByName['date_time_original']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => + columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => + columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get exposureTime => + columnsByName['exposure_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get fNumber => + columnsByName['f_number']! as i1.GeneratedColumn; + i1.GeneratedColumn get fileSize => + columnsByName['file_size']! as i1.GeneratedColumn; + i1.GeneratedColumn get focalLength => + columnsByName['focal_length']! as i1.GeneratedColumn; + i1.GeneratedColumn get latitude => + columnsByName['latitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get longitude => + columnsByName['longitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get iso => + columnsByName['iso']! as i1.GeneratedColumn; + i1.GeneratedColumn get make => + columnsByName['make']! as i1.GeneratedColumn; + i1.GeneratedColumn get model => + columnsByName['model']! as i1.GeneratedColumn; + i1.GeneratedColumn get lens => + columnsByName['lens']! as i1.GeneratedColumn; + i1.GeneratedColumn get orientation => + columnsByName['orientation']! as i1.GeneratedColumn; + i1.GeneratedColumn get timeZone => + columnsByName['time_zone']! as i1.GeneratedColumn; + i1.GeneratedColumn get rating => + columnsByName['rating']! as i1.GeneratedColumn; + i1.GeneratedColumn get projectionType => + columnsByName['projection_type']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_36(String aliasedName) => + i1.GeneratedColumn('asset_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); +i1.GeneratedColumn _column_37(String aliasedName) => + i1.GeneratedColumn('city', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_38(String aliasedName) => + i1.GeneratedColumn('state', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_39(String aliasedName) => + i1.GeneratedColumn('country', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_40(String aliasedName) => + i1.GeneratedColumn('date_time_original', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_41(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_42(String aliasedName) => + i1.GeneratedColumn('exposure_time', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_43(String aliasedName) => + i1.GeneratedColumn('f_number', aliasedName, true, + type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_44(String aliasedName) => + i1.GeneratedColumn('file_size', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_45(String aliasedName) => + i1.GeneratedColumn('focal_length', aliasedName, true, + type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_46(String aliasedName) => + i1.GeneratedColumn('latitude', aliasedName, true, + type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_47(String aliasedName) => + i1.GeneratedColumn('longitude', aliasedName, true, + type: i1.DriftSqlType.double); +i1.GeneratedColumn _column_48(String aliasedName) => + i1.GeneratedColumn('iso', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_49(String aliasedName) => + i1.GeneratedColumn('make', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_50(String aliasedName) => + i1.GeneratedColumn('model', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_51(String aliasedName) => + i1.GeneratedColumn('lens', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_52(String aliasedName) => + i1.GeneratedColumn('orientation', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_53(String aliasedName) => + i1.GeneratedColumn('time_zone', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_54(String aliasedName) => + i1.GeneratedColumn('rating', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_55(String aliasedName) => + i1.GeneratedColumn('projection_type', aliasedName, true, + type: i1.DriftSqlType.string); + +class Shape9 extends i0.VersionedTable { + Shape9({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbnailAssetId => + columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get isActivityEnabled => + columnsByName['is_activity_enabled']! as i1.GeneratedColumn; + i1.GeneratedColumn get order => + columnsByName['order']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_56(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, false, + type: i1.DriftSqlType.string, + defaultValue: const CustomExpression('\'\'')); +i1.GeneratedColumn _column_57(String aliasedName) => + i1.GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); +i1.GeneratedColumn _column_58(String aliasedName) => + i1.GeneratedColumn('is_activity_enabled', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); +i1.GeneratedColumn _column_59(String aliasedName) => + i1.GeneratedColumn('order', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_60(String aliasedName) => + i1.GeneratedColumn('album_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + +class Shape10 extends i0.VersionedTable { + Shape10({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get albumId => + columnsByName['album_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get userId => + columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get role => + columnsByName['role']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_61(String aliasedName) => + i1.GeneratedColumn('role', aliasedName, false, + type: i1.DriftSqlType.int); + +class Shape11 extends i0.VersionedTable { + Shape11({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get data => + columnsByName['data']! as i1.GeneratedColumn; + i1.GeneratedColumn get isSaved => + columnsByName['is_saved']! as i1.GeneratedColumn; + i1.GeneratedColumn get memoryAt => + columnsByName['memory_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get seenAt => + columnsByName['seen_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get showAt => + columnsByName['show_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get hideAt => + columnsByName['hide_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_62(String aliasedName) => + i1.GeneratedColumn('data', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_63(String aliasedName) => + i1.GeneratedColumn('is_saved', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_64(String aliasedName) => + i1.GeneratedColumn('memory_at', aliasedName, false, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_65(String aliasedName) => + i1.GeneratedColumn('seen_at', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_66(String aliasedName) => + i1.GeneratedColumn('show_at', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_67(String aliasedName) => + i1.GeneratedColumn('hide_at', aliasedName, true, + type: i1.DriftSqlType.dateTime); + +class Shape12 extends i0.VersionedTable { + Shape12({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get memoryId => + columnsByName['memory_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_68(String aliasedName) => + i1.GeneratedColumn('memory_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE')); + +class Shape13 extends i0.VersionedTable { + Shape13({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get faceAssetId => + columnsByName['face_asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbnailPath => + columnsByName['thumbnail_path']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get isHidden => + columnsByName['is_hidden']! as i1.GeneratedColumn; + i1.GeneratedColumn get color => + columnsByName['color']! as i1.GeneratedColumn; + i1.GeneratedColumn get birthDate => + columnsByName['birth_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_69(String aliasedName) => + i1.GeneratedColumn('face_asset_id', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_70(String aliasedName) => + i1.GeneratedColumn('thumbnail_path', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_71(String aliasedName) => + i1.GeneratedColumn('is_favorite', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))')); +i1.GeneratedColumn _column_72(String aliasedName) => + i1.GeneratedColumn('is_hidden', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))')); +i1.GeneratedColumn _column_73(String aliasedName) => + i1.GeneratedColumn('color', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_74(String aliasedName) => + i1.GeneratedColumn('birth_date', aliasedName, true, + type: i1.DriftSqlType.dateTime); + +final class Schema3 extends i0.VersionedSchema { + Schema3({required super.database}) : super(version: 3); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + ]; + late final Shape0 userEntity = Shape0( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 remoteAssetEntity = Shape1( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localAssetEntity = Shape2( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_75, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + final i1.Index uQRemoteAssetOwnerChecksum = i1.Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum', + 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(user_id, "key")', + ], + columns: [ + _column_25, + _column_26, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(shared_by_id, shared_with_id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 localAlbumEntity = Shape6( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_34, + _column_35, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id)', + ], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, album_id)', + ], + columns: [ + _column_36, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(album_id, user_id)', + ], + columns: [ + _column_60, + _column_25, + _column_61, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(asset_id, memory_id)', + ], + columns: [ + _column_36, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 personEntity = Shape13( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_70, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null); +} + +i1.GeneratedColumn _column_75(String aliasedName) => + i1.GeneratedColumn('primary_asset_id', aliasedName, false, + type: i1.DriftSqlType.string); +i0.MigrationStepWithVersion migrationSteps({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, + required Future Function(i1.Migrator m, Schema3 schema) from2To3, +}) { + return (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = Schema2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + case 2: + final schema = Schema3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }; +} + +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, + required Future Function(i1.Migrator m, Schema3 schema) from2To3, +}) => + i0.VersionedSchema.stepByStepHelper( + step: migrationSteps( + from1To2: from1To2, + from2To3: from2To3, + )); diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 44ebe7f7ca..5f192a20cf 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -5,9 +5,16 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/database.utils.dart'; import 'package:platform/platform.dart'; -enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum } +enum SortLocalAlbumsBy { + id, + backupSelection, + isIosSharedAlbum, + name, + assetCount +} class DriftLocalAlbumRepository extends DriftDatabaseRepository { final Drift _db; @@ -40,6 +47,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { OrderingTerm.asc(_db.localAlbumEntity.backupSelection), SortLocalAlbumsBy.isIosSharedAlbum => OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum), + SortLocalAlbumsBy.name => + OrderingTerm.asc(_db.localAlbumEntity.name), + SortLocalAlbumsBy.assetCount => OrderingTerm.desc(assetCount), }, ); } @@ -150,7 +160,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { batch.insert( _db.localAlbumEntity, companion, - onConflict: DoUpdate((_) => companion), + onConflict: DoUpdate( + (old) => LocalAlbumEntityCompanion( + id: companion.id, + name: companion.name, + updatedAt: companion.updatedAt, + isIosSharedAlbum: companion.isIosSharedAlbum, + marker_: companion.marker_, + ), + ), ); } }); @@ -381,30 +399,3 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return results.isNotEmpty ? results.first : null; } } - -extension on LocalAlbumEntityData { - LocalAlbum toDto({int assetCount = 0}) { - return LocalAlbum( - id: id, - name: name, - updatedAt: updatedAt, - assetCount: assetCount, - backupSelection: backupSelection, - ); - } -} - -extension on LocalAssetEntityData { - LocalAsset toDto() { - return LocalAsset( - id: id, - name: name, - checksum: checksum, - type: type, - createdAt: createdAt, - updatedAt: updatedAt, - durationInSeconds: durationInSeconds, - isFavorite: isFavorite, - ); - } -} diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index cb6871cd22..8d21c858a2 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; @@ -43,4 +44,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { } }); } + + Future delete(List ids) { + if (ids.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) { + for (final slice in ids.slices(32000)) { + batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice)); + } + }); + } + + Future getById(String id) { + final query = _db.localAssetEntity.select() + ..where((lae) => lae.id.equals(id)); + + return query.map((row) => row.toDto()).getSingleOrNull(); + } } diff --git a/mobile/lib/infrastructure/repositories/partner.repository.dart b/mobile/lib/infrastructure/repositories/partner.repository.dart new file mode 100644 index 0000000000..b3b057b035 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/partner.repository.dart @@ -0,0 +1,164 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftPartnerRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftPartnerRepository(this._db) : super(_db); + + Future> getPartners(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who we can share our library with + Future> getAvailablePartners(String currentUserId) { + final query = _db.select(_db.userEntity) + ..where((row) => row.id.equals(currentUserId).not()); + + return query.map((user) { + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: false, + ); + }).get(); + } + + // Get users who are sharing their photos WITH the current user + Future> getSharedWith(String partnerId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedWithId.equals(partnerId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + // Get users who the current user is sharing their photos TO + Future> getSharedBy(String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).get(); + } + + Future> getAllPartnerIds(String userId) async { + // Get users who are sharing with me (sharedWithId = userId) + final sharingWithMeQuery = _db.select(_db.partnerEntity) + ..where((tbl) => tbl.sharedWithId.equals(userId)); + final sharingWithMe = + await sharingWithMeQuery.map((row) => row.sharedById).get(); + + // Get users who I am sharing with (sharedById = userId) + final sharingWithThemQuery = _db.select(_db.partnerEntity) + ..where((tbl) => tbl.sharedById.equals(userId)); + final sharingWithThem = + await sharingWithThemQuery.map((row) => row.sharedWithId).get(); + + // Combine both lists and remove duplicates + final allPartnerIds = + {...sharingWithMe, ...sharingWithThem}.toList(); + return allPartnerIds; + } + + Future getPartner(String partnerId, String userId) { + final query = _db.select(_db.partnerEntity).join([ + innerJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById), + ), + ]) + ..where( + _db.partnerEntity.sharedById.equals(partnerId) & + _db.partnerEntity.sharedWithId.equals(userId), + ); + + return query.map((row) { + final user = row.readTable(_db.userEntity); + final partner = row.readTable(_db.partnerEntity); + return PartnerUserDto( + id: user.id, + email: user.email, + name: user.name, + inTimeline: partner.inTimeline, + ); + }).getSingleOrNull(); + } + + Future toggleShowInTimeline(PartnerUserDto partner, String userId) { + return _db.partnerEntity.update().replace( + PartnerEntityCompanion( + sharedById: Value(partner.id), + sharedWithId: Value(userId), + inTimeline: Value(!partner.inTimeline), + ), + ); + } + + Future create(String partnerId, String userId) { + final entity = PartnerEntityCompanion( + sharedById: Value(userId), + sharedWithId: Value(partnerId), + inTimeline: const Value(false), + ); + + return _db.partnerEntity.insertOne(entity); + } + + Future delete(String partnerId, String userId) { + return _db.partnerEntity.deleteWhere( + (t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId), + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/person.repository.dart b/mobile/lib/infrastructure/repositories/person.repository.dart new file mode 100644 index 0000000000..859765d63b --- /dev/null +++ b/mobile/lib/infrastructure/repositories/person.repository.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftPersonRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftPersonRepository(this._db) : super(_db); + + Future> getAll(String userId) { + final query = _db.personEntity.select() + ..where((e) => e.ownerId.equals(userId)); + + return query.map((person) { + return person.toDto(); + }).get(); + } +} + +extension on PersonEntityData { + Person toDto() { + return Person( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + name: name, + faceAssetId: faceAssetId, + thumbnailPath: thumbnailPath, + isFavorite: isFavorite, + isHidden: isHidden, + color: color, + birthDate: birthDate, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index b77184bce0..c3c4570559 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -1,7 +1,13 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; enum SortRemoteAlbumsBy { id, updatedAt } @@ -99,11 +105,169 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { }); } + Future update(RemoteAlbum album) async { + await _db.remoteAlbumEntity.update().replace( + RemoteAlbumEntityCompanion( + id: Value(album.id), + name: Value(album.name), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + description: Value(album.description), + thumbnailAssetId: Value(album.thumbnailAssetId), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order), + ), + ); + } + Future removeAssets(String albumId, List assetIds) { return _db.remoteAlbumAssetEntity.deleteWhere( (tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds), ); } + + FutureOr<(DateTime, DateTime)> getDateRange(String albumId) { + final query = _db.remoteAlbumAssetEntity.selectOnly() + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) + ..addColumns([ + _db.remoteAssetEntity.createdAt.min(), + _db.remoteAssetEntity.createdAt.max(), + ]) + ..join([ + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id + .equalsExp(_db.remoteAlbumAssetEntity.assetId), + ), + ]); + + return query.map((row) { + final minDate = row.read(_db.remoteAssetEntity.createdAt.min()); + final maxDate = row.read(_db.remoteAssetEntity.createdAt.max()); + return (minDate ?? DateTime.now(), maxDate ?? DateTime.now()); + }).getSingle(); + } + + Future> getSharedUsers(String albumId) async { + final albumUserRows = await (_db.select(_db.remoteAlbumUserEntity) + ..where((row) => row.albumId.equals(albumId))) + .get(); + + if (albumUserRows.isEmpty) { + return []; + } + + final userIds = albumUserRows.map((row) => row.userId); + + return (_db.select(_db.userEntity)..where((row) => row.id.isIn(userIds))) + .map( + (user) => UserDto( + id: user.id, + email: user.email, + name: user.name, + profileImagePath: user.profileImagePath?.isEmpty == true + ? null + : user.profileImagePath, + isAdmin: user.isAdmin, + updatedAt: user.updatedAt, + quotaSizeInBytes: user.quotaSizeInBytes ?? 0, + quotaUsageInBytes: user.quotaUsageInBytes, + memoryEnabled: true, + inTimeline: false, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + ), + ) + .get(); + } + + Future> getAssets(String albumId) { + final query = _db.remoteAlbumAssetEntity.select().join([ + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + ), + ]) + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)); + + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto()) + .get(); + } + + Future addAssets(String albumId, List assetIds) async { + final albumAssets = assetIds.map( + (assetId) => RemoteAlbumAssetEntityCompanion( + albumId: Value(albumId), + assetId: Value(assetId), + ), + ); + + await _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumAssetEntity, + albumAssets, + ); + }); + + return assetIds.length; + } + + Future addUsers(String albumId, List userIds) { + final albumUsers = userIds.map( + (assetId) => RemoteAlbumUserEntityCompanion( + albumId: Value(albumId), + userId: Value(assetId), + role: const Value(AlbumUserRole.editor), + ), + ); + + return _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumUserEntity, + albumUsers, + ); + }); + } + + Future deleteAlbum(String albumId) async { + return _db.transaction(() async { + await _db.remoteAlbumEntity.deleteWhere( + (table) => table.id.equals(albumId), + ); + }); + } + + Stream watchAlbum(String albumId) { + final query = _db.remoteAlbumEntity.select().join([ + leftOuterJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId), + useColumns: false, + ), + leftOuterJoin( + _db.userEntity, + _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), + useColumns: false, + ), + ]) + ..where(_db.remoteAlbumEntity.id.equals(albumId)) + ..addColumns([_db.userEntity.name]) + ..groupBy([_db.remoteAlbumEntity.id]); + + return query.map((row) { + final album = row.readTable(_db.remoteAlbumEntity).toDto( + ownerName: row.read(_db.userEntity.name)!, + ); + return album; + }).watchSingleOrNull(); + } } extension on RemoteAlbumEntityData { diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 1f6f1b0891..52cfe2e7c2 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -1,11 +1,13 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -30,25 +32,66 @@ class RemoteAssetRepository extends DriftDatabaseRepository { } Stream watchAsset(String id) { - final query = _db.remoteAssetEntity - .select() - .addColumns([_db.localAssetEntity.id]).join([ + final stackCountRef = _db.stackEntity.id.count(); + + final query = _db.remoteAssetEntity.select().addColumns([ + _db.localAssetEntity.id, + _db.stackEntity.primaryAssetId, + stackCountRef, + ]).join([ leftOuterJoin( _db.localAssetEntity, _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), useColumns: false, ), + leftOuterJoin( + _db.stackEntity, + _db.stackEntity.primaryAssetId.equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + leftOuterJoin( + _db.remoteAssetEntity.createAlias('stacked_assets'), + _db.stackEntity.id.equalsExp( + _db.remoteAssetEntity.createAlias('stacked_assets').stackId, + ), + useColumns: false, + ), ]) - ..where(_db.remoteAssetEntity.id.equals(id)); + ..where(_db.remoteAssetEntity.id.equals(id)) + ..groupBy([ + _db.remoteAssetEntity.id, + _db.localAssetEntity.id, + _db.stackEntity.primaryAssetId, + ]); return query.map((row) { final asset = row.readTable(_db.remoteAssetEntity).toDto(); + final primaryAssetId = row.read(_db.stackEntity.primaryAssetId); + final stackCount = + primaryAssetId == id ? (row.read(stackCountRef) ?? 0) : 0; + return asset.copyWith( localId: row.read(_db.localAssetEntity.id), + stackCount: stackCount, ); }).watchSingleOrNull(); } + Future> getStackChildren(RemoteAsset asset) { + if (asset.stackId == null) { + return Future.value([]); + } + + final query = _db.remoteAssetEntity.select() + ..where( + (row) => + row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not(), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); + + return query.map((row) => row.toDto()).get(); + } + Future getExif(String id) { return _db.managers.remoteExifEntity .filter((row) => row.assetId.id.equals(id)) @@ -146,4 +189,53 @@ class RemoteAssetRepository extends DriftDatabaseRepository { } }); } + + Future stack(String userId, StackResponse stack) { + return _db.transaction(() async { + final stackIds = await _db.managers.stackEntity + .filter((row) => row.primaryAssetId.isIn(stack.assetIds)) + .map((row) => row.id) + .get(); + + await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + + await _db.batch((batch) { + final companion = StackEntityCompanion( + ownerId: Value(userId), + primaryAssetId: Value(stack.primaryAssetId), + ); + + batch.insert( + _db.stackEntity, + companion.copyWith(id: Value(stack.id)), + onConflict: DoUpdate((_) => companion), + ); + + for (final assetId in stack.assetIds) { + batch.update( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion( + stackId: Value(stack.id), + ), + where: (e) => e.id.equals(assetId), + ); + } + }); + }); + } + + Future unStack(List stackIds) { + return _db.transaction(() async { + await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds)); + + // TODO: delete this after adding foreign key on stackId + await _db.batch((batch) { + batch.update( + _db.remoteAssetEntity, + const RemoteAssetEntityCompanion(stackId: Value(null)), + where: (e) => e.stackId.isIn(stackIds), + ); + }); + }); + } } diff --git a/mobile/lib/infrastructure/repositories/search_api.repository.dart b/mobile/lib/infrastructure/repositories/search_api.repository.dart new file mode 100644 index 0000000000..55604b885c --- /dev/null +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -0,0 +1,87 @@ +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' + hide AssetVisibility; +import 'package:immich_mobile/infrastructure/repositories/api.repository.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:openapi/api.dart'; + +class SearchApiRepository extends ApiRepository { + final SearchApi _api; + const SearchApiRepository(this._api); + + Future search(SearchFilter filter, int page) { + AssetTypeEnum? type; + if (filter.mediaType.index == AssetType.image.index) { + type = AssetTypeEnum.IMAGE; + } else if (filter.mediaType.index == AssetType.video.index) { + type = AssetTypeEnum.VIDEO; + } + + if (filter.context != null && filter.context!.isNotEmpty) { + return _api.searchSmart( + SmartSearchDto( + query: filter.context!, + language: filter.language, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + visibility: filter.display.isArchive + ? AssetVisibility.archive + : AssetVisibility.timeline, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + return _api.searchAssets( + MetadataSearchDto( + originalFileName: filter.filename != null && filter.filename!.isNotEmpty + ? filter.filename + : null, + country: filter.location.country, + description: + filter.description != null && filter.description!.isNotEmpty + ? filter.description + : null, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + visibility: filter.display.isArchive + ? AssetVisibility.archive + : AssetVisibility.timeline, + isFavorite: filter.display.isFavorite ? true : null, + isNotInAlbum: filter.display.isNotInAlbum ? true : null, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + Future?> getSearchSuggestions( + SearchSuggestionType type, { + String? country, + String? state, + String? make, + String? model, + }) => + _api.getSearchSuggestions( + type, + country: country, + state: state, + make: make, + model: model, + ); +} diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 5b511709cd..0cf4f20ba8 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -7,8 +8,9 @@ class StorageRepository { const StorageRepository(); Future getFileForAsset(String assetId) async { - final log = Logger('StorageRepository'); File? file; + final log = Logger('StorageRepository'); + try { final entity = await AssetEntity.fromId(assetId); file = await entity?.originFile; @@ -20,4 +22,48 @@ class StorageRepository { } return file; } + + Future getMotionFileForAsset(LocalAsset asset) async { + File? file; + final log = Logger('StorageRepository'); + + try { + final entity = await AssetEntity.fromId(asset.id); + file = await entity?.originFileWithSubtype; + if (file == null) { + log.warning( + "Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + ); + } + } catch (error, stackTrace) { + log.warning( + "Error getting motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + error, + stackTrace, + ); + } + return file; + } + + Future getAssetEntityForAsset(LocalAsset asset) async { + final log = Logger('StorageRepository'); + + AssetEntity? entity; + + try { + entity = await AssetEntity.fromId(asset.id); + if (entity == null) { + log.warning( + "Cannot get AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + ); + } + } catch (error, stackTrace) { + log.warning( + "Error getting AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", + error, + stackTrace, + ); + } + return entity; + } } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index d1ecbd580c..11d58663e0 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -57,6 +57,7 @@ class SyncApiRepository { SyncRequestType.stacksV1, SyncRequestType.partnerStacksV1, SyncRequestType.userMetadataV1, + SyncRequestType.peopleV1, ], ).toJson(), ); @@ -173,6 +174,8 @@ const _kResponseMap = { SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson, SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson, + SyncEntityType.personV1: SyncPersonV1.fromJson, + SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index f3f26bb01f..e141c387be 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; @@ -137,6 +138,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { deletedAt: Value(asset.deletedAt), visibility: Value(asset.visibility.toAssetVisibility()), livePhotoVideoId: Value(asset.livePhotoVideoId), + stackId: Value(asset.stackId), ); batch.insert( @@ -510,6 +512,47 @@ class SyncStreamRepository extends DriftDatabaseRepository { rethrow; } } + + Future updatePeopleV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final person in data) { + final companion = PersonEntityCompanion( + createdAt: Value(person.createdAt), + updatedAt: Value(person.updatedAt), + ownerId: Value(person.ownerId), + name: Value(person.name), + faceAssetId: Value(person.faceAssetId), + isFavorite: Value(person.isFavorite), + isHidden: Value(person.isHidden), + color: Value(person.color), + birthDate: Value(person.birthDate), + ); + + batch.insert( + _db.personEntity, + companion.copyWith(id: Value(person.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updatePeopleV1', error, stack); + rethrow; + } + } + + Future deletePeopleV1( + Iterable data, + ) async { + try { + await _db.personEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.personId)), + ); + } catch (error, stack) { + _logger.severe('Error: deletePeopleV1', error, stack); + } + } } extension on AssetTypeEnum { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index c3c7fc71ab..0c3eee59af 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; @@ -88,6 +89,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, livePhotoVideoId: row.livePhotoVideoId, + stackId: row.stackId, + stackCount: row.stackCount, ) : LocalAsset( id: row.localId!, @@ -164,15 +167,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), useColumns: false, ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum + .equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), ], ) + ..addColumns([_db.remoteAssetEntity.id]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query - .map((row) => row.readTable(_db.localAssetEntity).toDto()) - .get(); + return query.map((row) { + final asset = row.readTable(_db.localAssetEntity).toDto(); + return asset.copyWith( + remoteId: row.read(_db.remoteAssetEntity.id), + ); + }).get(); } TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( @@ -195,41 +208,75 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return _db.remoteAlbumAssetEntity .count(where: (row) => row.albumId.equals(albumId)) .map(_generateBuckets) - .watchSingle(); + .watch() + .map((results) => results.isNotEmpty ? results.first : []) + .handleError((error) { + return []; + }); } - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + return (_db.remoteAlbumEntity.select() + ..where((row) => row.id.equals(albumId))) + .watch() + .switchMap((albums) { + if (albums.isEmpty) { + return Stream.value([]); + } - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..join([ - innerJoin( - _db.remoteAlbumAssetEntity, - _db.remoteAlbumAssetEntity.assetId - .equalsExp(_db.remoteAssetEntity.id), - useColumns: false, - ), - ]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAlbumAssetEntity.albumId.equals(albumId), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); + final album = albums.first; + final isAscending = album.order == AlbumAssetOrder.asc; + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..join([ + innerJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId + .equalsExp(_db.remoteAssetEntity.id), + useColumns: false, + ), + ]) + ..where( + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAlbumAssetEntity.albumId.equals(albumId), + ) + ..groupBy([dateExp]); + + if (isAscending) { + query.orderBy([OrderingTerm.asc(dateExp)]); + } else { + query.orderBy([OrderingTerm.desc(dateExp)]); + } + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + }).handleError((error) { + // If there's an error (e.g., album was deleted), return empty buckets + return []; + }); } Future> _getRemoteAlbumBucketAssets( String albumId, { required int offset, required int count, - }) { + }) async { + final albumData = await (_db.remoteAlbumEntity.select() + ..where((row) => row.id.equals(albumId))) + .getSingleOrNull(); + + // If album doesn't exist (was deleted), return empty list + if (albumData == null) { + return []; + } + + final isAscending = albumData.order == AlbumAssetOrder.asc; + final query = _db.remoteAssetEntity.select().join( [ innerJoin( @@ -239,18 +286,30 @@ class DriftTimelineRepository extends DriftDatabaseRepository { useColumns: false, ), ], - ) - ..where( + )..where( _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId), - ) - ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) - ..limit(count, offset: offset); + ); + + if (isAscending) { + query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); + } else { + query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]); + } + + query.limit(count, offset: offset); + return query .map((row) => row.readTable(_db.remoteAssetEntity).toDto()) .get(); } + TimelineQuery fromAssets(List assets) => ( + bucketSource: () => Stream.value(_generateBuckets(assets.length)), + assetSource: (offset, count) => + Future.value(assets.skip(offset).take(count).toList()), + ); + TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( filter: (row) => diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index bf79c28361..f036fd9bc3 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -31,6 +31,7 @@ import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; +import 'package:immich_mobile/utils/licenses.dart'; import 'package:immich_mobile/utils/migration.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; @@ -92,12 +93,30 @@ Future initApp() async { initializeTimeZones(); + // Initialize the file downloader + + await FileDownloader().configure( + // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 + globalConfig: (Config.holdingQueue, (6, 6, 3)), + ); + await FileDownloader().trackTasksInGroup( downloadGroupLivePhoto, markDownloadedComplete: false, ); await FileDownloader().trackTasks(); + + LicenseRegistry.addLicense( + () async* { + for (final license in nonPubLicenses.entries) { + yield LicenseEntryWithLineBreaks( + [license.key], + license.value, + ); + } + }, + ); } class ImmichApp extends ConsumerStatefulWidget { @@ -159,7 +178,21 @@ class ImmichAppState extends ConsumerState } void _configureFileDownloaderNotifications() { - FileDownloader().configureNotification( + FileDownloader().configureNotificationForGroup( + downloadGroupImage, + running: TaskNotification( + 'downloading_media'.tr(), + '${'file_name'.tr()}: {filename}', + ), + complete: TaskNotification( + 'download_finished'.tr(), + '${'file_name'.tr()}: {filename}', + ), + progressBar: true, + ); + + FileDownloader().configureNotificationForGroup( + downloadGroupVideo, running: TaskNotification( 'downloading_media'.tr(), '${'file_name'.tr()}: {filename}', diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 835e6aff8f..efe6f923ad 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -237,7 +237,7 @@ class SearchFilter { String? filename; String? description; String? language; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -282,7 +282,7 @@ class SearchFilter { String? filename, String? description, String? language, - Set? people, + Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, diff --git a/mobile/lib/models/upload/share_intent_attachment.model.dart b/mobile/lib/models/upload/share_intent_attachment.model.dart index 1bdb5b6b48..7e57cf94d2 100644 --- a/mobile/lib/models/upload/share_intent_attachment.model.dart +++ b/mobile/lib/models/upload/share_intent_attachment.model.dart @@ -17,7 +17,7 @@ enum UploadStatus { notFound, failed, canceled, - waitingtoRetry, + waitingToRetry, paused, } diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart index b2100946e6..c453ace618 100644 --- a/mobile/lib/pages/album/album_control_button.dart +++ b/mobile/lib/pages/album/album_control_button.dart @@ -1,52 +1,40 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; -// ignore: must_be_immutable class AlbumControlButton extends ConsumerWidget { - void Function() onAddPhotosPressed; - void Function() onAddUsersPressed; + final void Function()? onAddPhotosPressed; + final void Function()? onAddUsersPressed; - AlbumControlButton({ + const AlbumControlButton({ super.key, - required this.onAddPhotosPressed, - required this.onAddUsersPressed, + this.onAddPhotosPressed, + this.onAddUsersPressed, }); @override Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authProvider).userId; - final isOwner = ref.watch( - currentAlbumProvider.select((album) { - return album?.ownerId == userId; - }), - ); - - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ + return SizedBox( + height: 36, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + if (onAddPhotosPressed != null) AlbumActionFilledButton( key: const ValueKey('add_photos_button'), iconData: Icons.add_photo_alternate_outlined, onPressed: onAddPhotosPressed, labelText: "add_photos".tr(), ), - if (isOwner) - AlbumActionFilledButton( - key: const ValueKey('add_users_button'), - iconData: Icons.person_add_alt_rounded, - onPressed: onAddUsersPressed, - labelText: "album_viewer_page_share_add_users".tr(), - ), - ], - ), + if (onAddUsersPressed != null) + AlbumActionFilledButton( + key: const ValueKey('add_users_button'), + iconData: Icons.person_add_alt_rounded, + onPressed: onAddUsersPressed, + labelText: "album_viewer_page_share_add_users".tr(), + ), + ], ), ); } diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart index 86b23fba30..2edf6082ac 100644 --- a/mobile/lib/pages/album/album_viewer.dart +++ b/mobile/lib/pages/album/album_viewer.dart @@ -41,6 +41,11 @@ class AlbumViewer extends HookConsumerWidget { final userId = ref.watch(authProvider).userId; final isMultiselecting = ref.watch(multiselectProvider); final isProcessing = useProcessingOverlay(); + final isOwner = ref.watch( + currentAlbumProvider.select((album) { + return album?.ownerId == userId; + }), + ); Future onRemoveFromAlbumPressed(Iterable assets) async { final bool isSuccess = @@ -138,10 +143,13 @@ class AlbumViewer extends HookConsumerWidget { ), const AlbumSharedUserIcons(), if (album.isRemote) - AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: onAddUsersPressed, + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: AlbumControlButton( + key: const ValueKey("albumControlButton"), + onAddPhotosPressed: onAddPhotosPressed, + onAddUsersPressed: isOwner ? onAddUsersPressed : null, + ), ), const SizedBox(height: 8), ], diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart new file mode 100644 index 0000000000..1b9ec8ad07 --- /dev/null +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -0,0 +1,273 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; + +@RoutePage() +class DriftBackupPage extends ConsumerStatefulWidget { + const DriftBackupPage({super.key}); + + @override + ConsumerState createState() => _DriftBackupPageState(); +} + +class _DriftBackupPageState extends ConsumerState { + @override + void initState() { + super.initState(); + ref.read(driftBackupProvider.notifier).getBackupStatus(); + } + + Future startBackup() async { + await ref.read(driftBackupProvider.notifier).getBackupStatus(); + await ref.read(driftBackupProvider.notifier).backup(); + } + + Future stopBackup() async { + await ref.read(driftBackupProvider.notifier).cancel(); + } + + @override + Widget build(BuildContext context) { + final selectedAlbum = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.selected, + ) + .toList(); + final uploadItems = ref.watch( + driftBackupProvider.select((state) => state.uploadItems), + ); + + return Scaffold( + appBar: AppBar( + elevation: 0, + title: Text( + "backup_controller_page_backup".t(), + ), + leading: IconButton( + onPressed: () { + context.maybePop(true); + }, + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, + ), + ), + ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16, + bottom: 32, + ), + child: ListView( + children: [ + const SizedBox(height: 8), + const _BackupAlbumSelectionCard(), + if (selectedAlbum.isNotEmpty) ...[ + const _TotalCard(), + const _BackupCard(), + const _RemainderCard(), + const Divider(), + BackupToggleButton( + onStart: () async => await startBackup(), + onStop: () async => await stopBackup(), + ), + if (uploadItems.isNotEmpty) + TextButton.icon( + icon: const Icon(Icons.info_outline_rounded), + onPressed: () => context.pushRoute( + const DriftUploadDetailRoute(), + ), + label: Text("view_details".t(context: context)), + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _BackupAlbumSelectionCard extends ConsumerWidget { + const _BackupAlbumSelectionCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Widget buildSelectedAlbumName() { + String text = "backup_controller_page_backup_selected".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.selected, + ) + .toList(); + + if (albums.isNotEmpty) { + for (var album in albums) { + if (album.name == "Recent" || album.name == "Recents") { + text += "${album.name} (${'all'.tr()}), "; + } else { + text += "${album.name}, "; + } + } + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + text.trim().substring(0, text.length - 2), + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, + ), + ), + ); + } else { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + "backup_controller_page_none_selected".tr(), + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, + ), + ), + ); + } + } + + Widget buildExcludedAlbumName() { + String text = "backup_controller_page_excluded".tr(); + final albums = ref + .watch(backupAlbumProvider) + .where( + (album) => album.backupSelection == BackupSelection.excluded, + ) + .toList(); + + if (albums.isNotEmpty) { + for (var album in albums) { + text += "${album.name}, "; + } + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + text.trim().substring(0, text.length - 2), + style: context.textTheme.labelLarge?.copyWith( + color: Colors.red[300], + ), + ), + ); + } else { + return const SizedBox(); + } + } + + return Card( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), + side: BorderSide( + color: context.colorScheme.outlineVariant, + width: 1, + ), + ), + elevation: 0, + borderOnForeground: false, + child: ListTile( + minVerticalPadding: 18, + title: Text( + "backup_controller_page_albums", + style: context.textTheme.titleMedium, + ).tr(), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "backup_controller_page_to_backup", + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ).tr(), + buildSelectedAlbumName(), + buildExcludedAlbumName(), + ], + ), + ), + trailing: ElevatedButton( + onPressed: () async { + await context.pushRoute(const DriftBackupAlbumSelectionRoute()); + ref.read(driftBackupProvider.notifier).getBackupStatus(); + }, + child: const Text( + "select", + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ), + ); + } +} + +class _TotalCard extends ConsumerWidget { + const _TotalCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final totalCount = + ref.watch(driftBackupProvider.select((p) => p.totalCount)); + + return BackupInfoCard( + title: "total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: totalCount.toString(), + ); + } +} + +class _BackupCard extends ConsumerWidget { + const _BackupCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final backupCount = + ref.watch(driftBackupProvider.select((p) => p.backupCount)); + + return BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: backupCount.toString(), + ); + } +} + +class _RemainderCard extends ConsumerWidget { + const _RemainderCard(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final remainderCount = + ref.watch(driftBackupProvider.select((p) => p.remainderCount)); + return BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), + info: remainderCount.toString(), + ); + } +} diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart new file mode 100644 index 0000000000..fd39f0a579 --- /dev/null +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -0,0 +1,523 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; + +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; + +@RoutePage() +class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget { + const DriftBackupAlbumSelectionPage({super.key}); + + @override + ConsumerState createState() => + _DriftBackupAlbumSelectionPageState(); +} + +class _DriftBackupAlbumSelectionPageState + extends ConsumerState { + String _searchQuery = ''; + bool _isSearchMode = false; + late ValueNotifier _enableSyncUploadAlbum; + late TextEditingController _searchController; + late FocusNode _searchFocusNode; + + @override + void initState() { + super.initState(); + _enableSyncUploadAlbum = ValueNotifier(false); + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + _enableSyncUploadAlbum.value = ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); + ref.read(backupAlbumProvider.notifier).getAll(); + } + + @override + void dispose() { + _enableSyncUploadAlbum.dispose(); + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final albums = ref.watch(backupAlbumProvider); + final albumCount = albums.length; + // Filter albums based on search query + final filteredAlbums = albums.where((album) { + if (_searchQuery.isEmpty) return true; + return album.name.toLowerCase().contains(_searchQuery.toLowerCase()); + }).toList(); + + final selectedBackupAlbums = albums + .where((album) => album.backupSelection == BackupSelection.selected) + .toList(); + final excludedBackupAlbums = albums + .where((album) => album.backupSelection == BackupSelection.excluded) + .toList(); + + handleSyncAlbumToggle(bool isEnable) async { + if (isEnable) { + await ref.read(albumProvider.notifier).refreshRemoteAlbums(); + for (final album in selectedBackupAlbums) { + await ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } + } + } + + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () => context.maybePop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + title: _isSearchMode + ? SearchField( + hintText: 'search_albums'.t(context: context), + autofocus: true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) => + setState(() => _searchQuery = value.trim()), + ) + : const Text( + "backup_album_selection_page_select_albums", + ).t(context: context), + actions: [ + if (!_isSearchMode) + IconButton( + icon: const Icon(Icons.search), + onPressed: () => setState(() { + _isSearchMode = true; + _searchQuery = ''; + }), + ) + else + IconButton( + icon: const Icon(Icons.close), + onPressed: () => setState(() { + _isSearchMode = false; + _searchQuery = ''; + _searchController.clear(); + }), + ), + ], + elevation: 0, + ), + body: CustomScrollView( + physics: const ClampingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: Text( + "backup_album_selection_page_selection_info", + style: context.textTheme.titleSmall, + ).t(context: context), + ), + // Selected Album Chips + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Wrap( + children: [ + _SelectedAlbumNameChips( + selectedBackupAlbums: selectedBackupAlbums, + ), + _ExcludedAlbumNameChips( + excludedBackupAlbums: excludedBackupAlbums, + ), + ], + ), + ), + + SettingsSwitchListTile( + valueNotifier: _enableSyncUploadAlbum, + title: "sync_albums".t(context: context), + subtitle: + "sync_upload_album_setting_subtitle".t(context: context), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + titleStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + subtitleStyle: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.primary, + ), + onChanged: handleSyncAlbumToggle, + ), + + ListTile( + title: Text( + "albums_on_device_count".t( + context: context, + args: {'count': albumCount.toString()}, + ), + style: context.textTheme.titleSmall, + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + "backup_album_selection_page_albums_tap", + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, + ), + ).t(context: context), + ), + trailing: IconButton( + splashRadius: 16, + icon: Icon( + Icons.info, + size: 20, + color: context.primaryColor, + ), + onPressed: () { + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(10)), + ), + elevation: 5, + title: Text( + 'backup_album_selection_page_selection_info', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: context.primaryColor, + ), + ).t(context: context), + content: SingleChildScrollView( + child: ListBody( + children: [ + const Text( + 'backup_album_selection_page_assets_scatter', + style: TextStyle( + fontSize: 14, + ), + ).t(context: context), + ], + ), + ), + ); + }, + ); + }, + ), + ), + + if (Platform.isAndroid) + _SelectAllButton( + filteredAlbums: filteredAlbums, + selectedBackupAlbums: selectedBackupAlbums, + ), + ], + ), + ), + SliverLayoutBuilder( + builder: (context, constraints) { + if (constraints.crossAxisExtent > 600) { + return _AlbumSelectionGrid( + filteredAlbums: filteredAlbums, + searchQuery: _searchQuery, + ); + } else { + return _AlbumSelectionList( + filteredAlbums: filteredAlbums, + searchQuery: _searchQuery, + ); + } + }, + ), + ], + ), + ); + } +} + +class _AlbumSelectionList extends StatelessWidget { + final List filteredAlbums; + final String searchQuery; + + const _AlbumSelectionList({ + required this.filteredAlbums, + required this.searchQuery, + }); + + @override + Widget build(BuildContext context) { + if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Text('album_search_not_found'.t(context: context)), + ), + ), + ); + } + + if (filteredAlbums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + ((context, index) { + return DriftAlbumInfoListTile( + album: filteredAlbums[index], + ); + }), + childCount: filteredAlbums.length, + ), + ), + ); + } +} + +class _AlbumSelectionGrid extends StatelessWidget { + final List filteredAlbums; + final String searchQuery; + + const _AlbumSelectionGrid({ + required this.filteredAlbums, + required this.searchQuery, + }); + + @override + Widget build(BuildContext context) { + if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) { + return SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Text('album_search_not_found'.t(context: context)), + ), + ), + ); + } + + if (filteredAlbums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 300, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + ), + itemCount: filteredAlbums.length, + itemBuilder: ((context, index) { + return DriftAlbumInfoListTile( + album: filteredAlbums[index], + ); + }), + ), + ); + } +} + +class _SelectedAlbumNameChips extends ConsumerWidget { + final List selectedBackupAlbums; + + const _SelectedAlbumNameChips({ + required this.selectedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Wrap( + children: selectedBackupAlbums.asMap().entries.map((entry) { + final album = entry.value; + + void removeSelection() { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } + + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: removeSelection, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: Chip( + label: Text( + album.name, + style: TextStyle( + fontSize: 12, + color: context.isDarkTheme ? Colors.black : Colors.white, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: context.primaryColor, + deleteIconColor: + context.isDarkTheme ? Colors.black : Colors.white, + deleteIcon: const Icon( + Icons.cancel_rounded, + size: 15, + ), + onDeleted: removeSelection, + ), + ), + ), + ); + }).toList(), + ); + } +} + +class _ExcludedAlbumNameChips extends ConsumerWidget { + final List excludedBackupAlbums; + + const _ExcludedAlbumNameChips({ + required this.excludedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Wrap( + children: excludedBackupAlbums.asMap().entries.map((entry) { + final album = entry.value; + + void removeSelection() { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } + + return GestureDetector( + onTap: removeSelection, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: Chip( + label: Text( + album.name, + style: TextStyle( + fontSize: 12, + color: context.scaffoldBackgroundColor, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Colors.red[300], + deleteIconColor: context.scaffoldBackgroundColor, + deleteIcon: const Icon( + Icons.cancel_rounded, + size: 15, + ), + onDeleted: removeSelection, + ), + ), + ), + ); + }).toList(), + ); + } +} + +class _SelectAllButton extends ConsumerWidget { + final List filteredAlbums; + final List selectedBackupAlbums; + + const _SelectAllButton({ + required this.filteredAlbums, + required this.selectedBackupAlbums, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final canSelectAll = filteredAlbums + .where((album) => album.backupSelection != BackupSelection.selected) + .isNotEmpty; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: canSelectAll + ? () { + for (final album in filteredAlbums) { + if (album.backupSelection != BackupSelection.selected) { + ref + .read(backupAlbumProvider.notifier) + .selectAlbum(album); + } + } + } + : null, + icon: const Icon(Icons.select_all), + label: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + "select_all".t(context: context), + ), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12.0), + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: OutlinedButton.icon( + onPressed: selectedBackupAlbums.isNotEmpty + ? () { + for (final album in filteredAlbums) { + if (album.backupSelection == BackupSelection.selected) { + ref + .read(backupAlbumProvider.notifier) + .deselectAlbum(album); + } + } + } + : null, + icon: const Icon(Icons.deselect), + label: Text('deselect_all'.t(context: context)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12.0), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart new file mode 100644 index 0000000000..66803265e6 --- /dev/null +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -0,0 +1,428 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:path/path.dart' as path; + +@RoutePage() +class DriftUploadDetailPage extends ConsumerWidget { + const DriftUploadDetailPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uploadItems = ref.watch( + driftBackupProvider.select((state) => state.uploadItems), + ); + + return Scaffold( + appBar: AppBar( + title: Text("upload_details".t(context: context)), + backgroundColor: context.colorScheme.surface, + elevation: 0, + scrolledUnderElevation: 1, + ), + body: uploadItems.isEmpty + ? _buildEmptyState(context) + : _buildUploadList(uploadItems), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_upload_outlined, + size: 80, + color: context.colorScheme.onSurface.withValues(alpha: 0.3), + ), + const SizedBox(height: 16), + Text( + "no_uploads_in_progress".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ); + } + + Widget _buildUploadList( + Map uploadItems, + ) { + return ListView.separated( + addAutomaticKeepAlives: true, + padding: const EdgeInsets.all(16), + itemCount: uploadItems.length, + separatorBuilder: (context, index) => const SizedBox(height: 4), + itemBuilder: (context, index) { + final item = uploadItems.values.elementAt(index); + return _buildUploadCard(context, item); + }, + ); + } + + Widget _buildUploadCard( + BuildContext context, + DriftUploadStatus item, + ) { + final isCompleted = item.progress >= 1.0; + final double progressPercentage = (item.progress * 100).clamp(0, 100); + + return Card( + elevation: 0, + color: context.colorScheme.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + side: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: InkWell( + onTap: () => _showFileDetailDialog(context, item), + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + path.basename(item.filename), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Tap for more details', + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.onSurface + .withValues(alpha: 0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + _buildProgressIndicator( + context, + item.progress, + progressPercentage, + isCompleted, + item.networkSpeedAsString, + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildProgressIndicator( + BuildContext context, + double progress, + double percentage, + bool isCompleted, + String networkSpeedAsString, + ) { + return Column( + children: [ + Stack( + alignment: AlignmentDirectional.center, + children: [ + SizedBox( + width: 36, + height: 36, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: progress), + duration: const Duration(milliseconds: 300), + builder: (context, value, _) => CircularProgressIndicator( + backgroundColor: + context.colorScheme.outline.withValues(alpha: 0.2), + strokeWidth: 3, + value: value, + color: isCompleted + ? context.colorScheme.primary + : context.colorScheme.secondary, + ), + ), + ), + if (isCompleted) + Icon( + Icons.check_circle_rounded, + size: 28, + color: context.colorScheme.primary, + ) + else + Text( + percentage.toStringAsFixed(0), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ], + ), + Text( + networkSpeedAsString, + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + fontSize: 10, + ), + ), + ], + ); + } + + Future _showFileDetailDialog( + BuildContext context, + DriftUploadStatus item, + ) async { + showDialog( + context: context, + builder: (context) => FileDetailDialog(uploadStatus: item), + ); + } +} + +class FileDetailDialog extends ConsumerWidget { + final DriftUploadStatus uploadStatus; + + const FileDetailDialog({ + super.key, + required this.uploadStatus, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + insetPadding: const EdgeInsets.all(20), + backgroundColor: context.colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), + side: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + title: Row( + children: [ + Icon( + Icons.info_outline, + color: context.primaryColor, + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + "details".t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ), + ], + ), + content: SizedBox( + width: double.maxFinite, + child: FutureBuilder( + future: _getAssetDetails(ref, uploadStatus.taskId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ); + } + + final asset = snapshot.data; + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Thumbnail at the top center + Center( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + width: 128, + height: 128, + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.outline + .withValues(alpha: 0.2), + width: 1, + ), + borderRadius: + const BorderRadius.all(Radius.circular(12)), + ), + child: asset != null + ? Thumbnail( + asset: asset, + size: const Size(512, 512), + fit: BoxFit.cover, + ) + : null, + ), + ), + ), + const SizedBox(height: 24), + if (asset != null) ...[ + _buildInfoSection(context, [ + _buildInfoRow( + context, + "Filename", + path.basename(uploadStatus.filename), + ), + _buildInfoRow( + context, + "Local ID", + asset.id, + ), + _buildInfoRow( + context, + "File Size", + formatHumanReadableBytes(uploadStatus.fileSize, 2), + ), + if (asset.width != null) + _buildInfoRow(context, "Width", "${asset.width}px"), + if (asset.height != null) + _buildInfoRow( + context, + "Height", + "${asset.height}px", + ), + _buildInfoRow( + context, + "Created At", + asset.createdAt.toString(), + ), + _buildInfoRow( + context, + "Updated At", + asset.updatedAt.toString(), + ), + if (asset.checksum != null) + _buildInfoRow( + context, + "Checksum", + asset.checksum!, + ), + ]), + ], + ], + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + "close".t(), + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ), + ], + ); + } + + Widget _buildInfoSection( + BuildContext context, + List children, + ) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + border: Border.all( + color: context.colorScheme.outline.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...children, + ], + ), + ); + } + + Widget _buildInfoRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + "$label:", + style: context.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ), + Expanded( + child: Text( + value, + style: context.textTheme.labelMedium?.copyWith(), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + Future _getAssetDetails( + WidgetRef ref, + String localAssetId, + ) async { + try { + final repository = ref.read(localAssetRepository); + return await repository.getById(localAssetId); + } catch (e) { + return null; + } + } +} diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart new file mode 100644 index 0000000000..a8569b25a0 --- /dev/null +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -0,0 +1,142 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/migration.dart'; +import 'package:permission_handler/permission_handler.dart'; + +@RoutePage() +class ChangeExperiencePage extends ConsumerStatefulWidget { + final bool switchingToBeta; + + const ChangeExperiencePage({super.key, required this.switchingToBeta}); + + @override + ConsumerState createState() => _ChangeExperiencePageState(); +} + +class _ChangeExperiencePageState extends ConsumerState { + bool hasMigrated = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration()); + } + + Future _handleMigration() async { + if (widget.switchingToBeta) { + final assetNotifier = ref.read(assetProvider.notifier); + if (assetNotifier.mounted) { + assetNotifier.dispose(); + } + final albumNotifier = ref.read(albumProvider.notifier); + if (albumNotifier.mounted) { + albumNotifier.dispose(); + } + + ref.read(websocketProvider.notifier).stopListenToOldEvents(); + ref.read(websocketProvider.notifier).startListeningToBetaEvents(); + + final permission = await ref + .read(galleryPermissionNotifier.notifier) + .requestGalleryPermission(); + + if (permission.isGranted) { + await ref.read(backgroundSyncProvider).syncLocal(full: true); + await migrateDeviceAssetToSqlite( + ref.read(isarProvider), + ref.read(driftProvider), + ); + await migrateBackupAlbumsToSqlite( + ref.read(isarProvider), + ref.read(driftProvider), + ); + } + } else { + await ref.read(backgroundSyncProvider).cancel(); + ref.read(websocketProvider.notifier).stopListeningToBetaEvents(); + ref.read(websocketProvider.notifier).startListeningToOldEvents(); + } + + if (mounted) { + setState(() { + HapticFeedback.heavyImpact(); + hasMigrated = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: Durations.long4, + child: hasMigrated + ? const Icon( + Icons.check_circle_rounded, + color: Colors.green, + size: 48.0, + ) + : const SizedBox( + width: 50.0, + height: 50.0, + child: CircularProgressIndicator(), + ), + ), + const SizedBox(height: 16.0), + Center( + child: Column( + children: [ + SizedBox( + width: 300.0, + child: AnimatedSwitcher( + duration: Durations.long4, + child: hasMigrated + ? Text( + "Migration success!", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ) + : Text( + "Data migration in progress...\nPlease wait and don't close this page", + style: context.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + ), + if (hasMigrated) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ElevatedButton( + onPressed: () { + context.replaceRoute( + widget.switchingToBeta + ? const TabShellRoute() + : const TabControllerRoute(), + ); + }, + child: const Text("Continue"), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 6fdbecced1..539406365a 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -125,7 +125,7 @@ class GalleryViewerPage extends HookConsumerWidget { final asset = loadAsset(currentIndex.value); if (asset.isRemote) { - ref.read(castProvider.notifier).loadMedia(asset, false); + ref.read(castProvider.notifier).loadMediaOld(asset, false); } else { if (isCasting) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -394,7 +394,7 @@ class GalleryViewerPage extends HookConsumerWidget { // send image to casting if the server has it if (newAsset.isRemote) { - ref.read(castProvider.notifier).loadMedia(newAsset, false); + ref.read(castProvider.notifier).loadMediaOld(newAsset, false); } else { context.scaffoldMessenger.clearSnackBars(); diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 6fc4cf5d88..e45001270c 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; +import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; @@ -94,55 +95,59 @@ class _MobileLayout extends StatelessWidget { const _MobileLayout(); @override Widget build(BuildContext context) { - return ListView( - physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 10.0), - children: SettingSection.values - .map( - (setting) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, + final List settings = SettingSection.values + .map( + (setting) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Card( + elevation: 0, + clipBehavior: Clip.antiAlias, + color: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), ), - child: Card( - elevation: 0, - clipBehavior: Clip.antiAlias, - color: context.colorScheme.surfaceContainer, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), + margin: const EdgeInsets.symmetric(vertical: 4.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, ), - margin: const EdgeInsets.symmetric(vertical: 4.0), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, + leading: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: context.isDarkTheme + ? Colors.black26 + : Colors.white.withAlpha(100), ), - leading: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16)), - color: context.isDarkTheme - ? Colors.black26 - : Colors.white.withAlpha(100), - ), - padding: const EdgeInsets.all(16.0), - child: Icon(setting.icon, color: context.primaryColor), - ), - title: Text( - setting.title, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - subtitle: Text( - setting.subtitle, - style: context.textTheme.labelLarge, - ).tr(), - onTap: () => - context.pushRoute(SettingsSubRoute(section: setting)), + padding: const EdgeInsets.all(16.0), + child: Icon(setting.icon, color: context.primaryColor), ), + title: Text( + setting.title, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + subtitle: Text( + setting.subtitle, + style: context.textTheme.labelLarge, + ).tr(), + onTap: () => + context.pushRoute(SettingsSubRoute(section: setting)), ), ), - ) - .toList(), + ), + ) + .toList(); + return ListView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.only(top: 10.0, bottom: 56), + children: [ + const BetaTimelineListTile(), + ...settings, + ], ); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 4b7a10d612..598e920651 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -73,7 +73,15 @@ class SplashScreenPageState extends ConsumerState { } if (context.router.current.name == SplashScreenRoute.name) { - context.replaceRoute(const TabControllerRoute()); + context.replaceRoute( + Store.isBetaTimelineEnabled + ? const TabShellRoute() + : const TabControllerRoute(), + ); + } + + if (Store.isBetaTimelineEnabled) { + return; } final hasPermission = diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index edaec6d336..b0be136a15 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -3,45 +3,50 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/migration.dart'; @RoutePage() -class TabShellPage extends ConsumerWidget { +class TabShellPage extends ConsumerStatefulWidget { const TabShellPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final isScreenLandscape = context.orientation == Orientation.landscape; + ConsumerState createState() => _TabShellPageState(); +} - Widget buildIcon({required Widget icon, required bool isProcessing}) { - if (!isProcessing) return icon; - return Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - icon, - Positioned( - right: -18, - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - context.primaryColor, - ), - ), - ), - ), - ], - ); - } +class _TabShellPageState extends ConsumerState { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + ref.read(websocketProvider.notifier).connect(); + + final isEnableBackup = ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableBackup); + + await runNewSync(ref, full: true).then((_) async { + if (isEnableBackup) { + await ref.read(driftBackupProvider.notifier).handleBackupResume(); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + final isScreenLandscape = context.orientation == Orientation.landscape; final navigationDestinations = [ NavigationDestination( @@ -49,12 +54,9 @@ class TabShellPage extends ConsumerWidget { icon: const Icon( Icons.photo_library_outlined, ), - selectedIcon: buildIcon( - isProcessing: false, - icon: Icon( - Icons.photo_library, - color: context.primaryColor, - ), + selectedIcon: Icon( + Icons.photo_library, + color: context.primaryColor, ), ), NavigationDestination( @@ -72,12 +74,9 @@ class TabShellPage extends ConsumerWidget { icon: const Icon( Icons.photo_album_outlined, ), - selectedIcon: buildIcon( - isProcessing: false, - icon: Icon( - Icons.photo_album_rounded, - color: context.primaryColor, - ), + selectedIcon: Icon( + Icons.photo_album_rounded, + color: context.primaryColor, ), ), NavigationDestination( @@ -85,12 +84,9 @@ class TabShellPage extends ConsumerWidget { icon: const Icon( Icons.space_dashboard_outlined, ), - selectedIcon: buildIcon( - isProcessing: false, - icon: Icon( - Icons.space_dashboard_rounded, - color: context.primaryColor, - ), + selectedIcon: Icon( + Icons.space_dashboard_rounded, + color: context.primaryColor, ), ), ]; @@ -117,7 +113,7 @@ class TabShellPage extends ConsumerWidget { return AutoTabsRouter( routes: [ const MainTimelineRoute(), - SearchRoute(), + DriftSearchRoute(), const DriftAlbumsRoute(), const DriftLibraryRoute(), ], @@ -167,7 +163,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { // Album page if (index == 2) { - ref.read(remoteAlbumProvider.notifier).getAll(); + ref.read(remoteAlbumProvider.notifier).refresh(); } ref.read(hapticFeedbackProvider.notifier).selectionClick(); diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index cca0e3b7ac..9bfd96ed74 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -1,13 +1,14 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; import 'package:immich_mobile/widgets/forms/pin_verification_form.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; @RoutePage() class PinAuthPage extends HookConsumerWidget { @@ -19,6 +20,7 @@ class PinAuthPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final localAuthState = ref.watch(localAuthProvider); final showPinRegistrationForm = useState(createPinCode); + final isBetaTimeline = Store.isBetaTimelineEnabled; Future registerBiometric(String pinCode) async { final isRegistered = @@ -39,7 +41,11 @@ class PinAuthPage extends HookConsumerWidget { ), ); - context.replaceRoute(const LockedRoute()); + if (isBetaTimeline) { + context.replaceRoute(const DriftLockedFolderRoute()); + } else { + context.replaceRoute(const LockedRoute()); + } } } @@ -93,8 +99,14 @@ class PinAuthPage extends HookConsumerWidget { Center( child: PinVerificationForm( autoFocus: true, - onSuccess: (_) => - context.replaceRoute(const LockedRoute()), + onSuccess: (_) { + if (isBetaTimeline) { + context + .replaceRoute(const DriftLockedFolderRoute()); + } else { + context.replaceRoute(const LockedRoute()); + } + }, ), ), const SizedBox(height: 24), diff --git a/mobile/lib/pages/library/partner/drift_partner.page.dart b/mobile/lib/pages/library/partner/drift_partner.page.dart new file mode 100644 index 0000000000..04efbe066c --- /dev/null +++ b/mobile/lib/pages/library/partner/drift_partner.page.dart @@ -0,0 +1,161 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +@RoutePage() +class DriftPartnerPage extends HookConsumerWidget { + const DriftPartnerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final potentialPartnersAsync = ref.watch(driftAvailablePartnerProvider); + + addNewUsersHandler() async { + final potentialPartners = potentialPartnersAsync.value; + if (potentialPartners == null || potentialPartners.isEmpty) { + ImmichToast.show( + context: context, + msg: "partner_page_no_more_users".tr(), + ); + return; + } + + final selectedUser = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("partner_page_select_partner").tr(), + children: [ + for (PartnerUserDto partner in potentialPartners) + SimpleDialogOption( + onPressed: () => context.pop(partner), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: PartnerUserAvatar(partner: partner), + ), + Text(partner.name), + ], + ), + ), + ], + ); + }, + ); + if (selectedUser != null) { + await ref.read(partnerUsersProvider.notifier).addPartner(selectedUser); + } + } + + onDeleteUser(PartnerUserDto partner) { + return showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmDialog( + title: "stop_photo_sharing", + content: "partner_page_stop_sharing_content" + .tr(namedArgs: {'partner': partner.name}), + onOk: () => + ref.read(partnerUsersProvider.notifier).removePartner(partner), + ); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text("partners").t(context: context), + elevation: 0, + centerTitle: false, + actions: [ + IconButton( + onPressed: potentialPartnersAsync.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + tooltip: "add_partner".tr(), + ), + ], + ), + body: _SharedToPartnerList( + onAddPartner: addNewUsersHandler, + onDeletePartner: onDeleteUser, + ), + ); + } +} + +class _SharedToPartnerList extends ConsumerWidget { + final VoidCallback onAddPartner; + final Function(PartnerUserDto partner) onDeletePartner; + + const _SharedToPartnerList({ + required this.onAddPartner, + required this.onDeletePartner, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final partnerAsync = ref.watch(driftSharedByPartnerProvider); + + return partnerAsync.when( + data: (partners) { + if (partners.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: const Text( + "partner_page_empty_message", + style: TextStyle(fontSize: 14), + ).tr(), + ), + Align( + alignment: Alignment.center, + child: ElevatedButton.icon( + onPressed: onAddPartner, + icon: const Icon(Icons.person_add), + label: const Text("add_partner").tr(), + ), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: partners.length, + itemBuilder: (context, index) { + final partner = partners[index]; + return ListTile( + leading: PartnerUserAvatar(partner: partner), + title: Text(partner.name), + subtitle: Text(partner.email), + trailing: IconButton( + icon: const Icon(Icons.person_remove), + onPressed: () => onDeletePartner(partner), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Text("Error loading partners: $error"), + ), + ); + } +} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index dba3199fac..3e5c153f88 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget { ); showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 3ff1b0c8ce..299ffe5497 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -75,7 +76,9 @@ class ShareIntentPage extends HookConsumerWidget { leading: IconButton( onPressed: () { context.navigateTo( - const TabControllerRoute(), + Store.isBetaTimelineEnabled + ? const TabShellRoute() + : const TabControllerRoute(), ); }, icon: const Icon(Icons.arrow_back), @@ -265,7 +268,7 @@ class UploadStatusIcon extends StatelessWidget { color: Colors.red, semanticLabel: 'canceled'.tr(), ), - UploadStatus.waitingtoRetry || UploadStatus.paused => Icon( + UploadStatus.waitingToRetry || UploadStatus.paused => Icon( Icons.pause_circle_rounded, color: context.primaryColor, semanticLabel: 'paused'.tr(), diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index bdb426fe22..7ee151f94d 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -22,16 +22,6 @@ final _features = [ icon: Icons.timeline_rounded, onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), ), - _Feature( - name: 'Video', - icon: Icons.video_collection_outlined, - onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()), - ), - _Feature( - name: 'Recently Taken', - icon: Icons.schedule_outlined, - onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()), - ), _Feature( name: 'Selection Mode Timeline', icon: Icons.developer_mode_rounded, @@ -101,6 +91,10 @@ final _features = [ ), _Feature( name: 'Clear Local Data', + style: const TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), icon: Icons.delete_forever_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); @@ -111,6 +105,10 @@ final _features = [ ), _Feature( name: 'Clear Remote Data', + style: const TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), icon: Icons.delete_sweep_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); @@ -122,21 +120,34 @@ final _features = [ await db.memoryEntity.deleteAll(); await db.memoryAssetEntity.deleteAll(); await db.stackEntity.deleteAll(); + await db.personEntity.deleteAll(); }, ), _Feature( name: 'Local Media Summary', + style: const TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + ), icon: Icons.table_chart_rounded, onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), ), _Feature( name: 'Remote Media Summary', + style: const TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + ), icon: Icons.summarize_rounded, onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), ), _Feature( name: 'Reset Sqlite', icon: Icons.table_view_rounded, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), onTap: (_, ref) async { final drift = ref.read(driftProvider); // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member @@ -169,7 +180,10 @@ class FeatInDevPage extends StatelessWidget { final feat = _features[index]; return Consumer( builder: (ctx, ref, _) => ListTile( - title: Text(feat.name), + title: Text( + feat.name, + style: feat.style, + ), trailing: Icon(feat.icon), visualDensity: VisualDensity.compact, onTap: () => unawaited(feat.onTap(ctx, ref)), @@ -192,10 +206,12 @@ class _Feature { required this.name, required this.icon, required this.onTap, + this.style, }); final String name; final IconData icon; + final TextStyle? style; final Future Function(BuildContext, WidgetRef _) onTap; } diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 7216b638e1..0582399eaf 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -13,7 +13,7 @@ class MainTimelinePage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); - return memoryLaneProvider.when( + return memoryLaneProvider.maybeWhen( data: (memories) { return memories.isEmpty ? const Timeline(showStorageIndicator: true) @@ -26,8 +26,7 @@ class MainTimelinePage extends ConsumerWidget { showStorageIndicator: true, ); }, - loading: () => const Timeline(showStorageIndicator: true), - error: (error, stackTrace) => const Timeline(showStorageIndicator: true), + orElse: () => const Timeline(showStorageIndicator: true), ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index e61dcdf90d..acd7b219b3 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -166,6 +166,10 @@ final _remoteStats = [ name: 'Stacks', load: (db) => db.managers.stackEntity.count(), ), + _Stat( + name: 'People', + load: (db) => db.managers.personEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index e6d3d796a4..c7dffbeaef 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/remote_album.utils.dart'; @@ -39,7 +40,7 @@ class _DriftAlbumsPageState extends ConsumerState { // Load albums when component mounts WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(remoteAlbumProvider.notifier).getAll(); + ref.read(remoteAlbumProvider.notifier).refresh(); }); searchController.addListener(() { @@ -87,17 +88,20 @@ class _DriftAlbumsPageState extends ConsumerState { @override Widget build(BuildContext context) { - final albumState = ref.watch(remoteAlbumProvider); - final albums = albumState.filteredAlbums; - final isLoading = albumState.isLoading; - final error = albumState.error; + final albums = + ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums)); + final userId = ref.watch(currentUserProvider)?.id; return RefreshIndicator( onRefresh: onRefresh, + edgeOffset: 100, child: CustomScrollView( slivers: [ ImmichSliverAppBar( + snap: false, + floating: false, + pinned: true, actions: [ IconButton( icon: const Icon( @@ -132,14 +136,10 @@ class _DriftAlbumsPageState extends ConsumerState { ? _AlbumGrid( albums: albums, userId: userId, - isLoading: isLoading, - error: error, ) : _AlbumList( albums: albums, userId: userId, - isLoading: isLoading, - error: error, ), ], ), @@ -478,48 +478,17 @@ class _QuickSortAndViewMode extends StatelessWidget { } } -class _AlbumList extends StatelessWidget { +class _AlbumList extends ConsumerWidget { const _AlbumList({ - required this.isLoading, - required this.error, required this.albums, required this.userId, }); - final bool isLoading; - final String? error; final List albums; final String? userId; @override - Widget build(BuildContext context) { - if (isLoading) { - return const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: CircularProgressIndicator(), - ), - ), - ); - } - - if (error != null) { - return SliverToBoxAdapter( - child: Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Text( - 'Error loading albums: $error', - style: TextStyle( - color: context.colorScheme.error, - ), - ), - ), - ), - ); - } - + Widget build(BuildContext context, WidgetRef ref) { if (albums.isEmpty) { return const SliverToBoxAdapter( child: Center( @@ -567,9 +536,12 @@ class _AlbumList extends StatelessWidget { color: context.colorScheme.onSurfaceSecondary, ), ), - onTap: () => context.router.push( - RemoteAlbumRoute(album: album), - ), + onTap: () { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + context.router.push( + RemoteAlbumRoute(album: album), + ); + }, leadingPadding: const EdgeInsets.only( right: 16, ), @@ -619,44 +591,13 @@ class _AlbumGrid extends StatelessWidget { const _AlbumGrid({ required this.albums, required this.userId, - required this.isLoading, - required this.error, }); final List albums; final String? userId; - final bool isLoading; - final String? error; @override Widget build(BuildContext context) { - if (isLoading) { - return const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: CircularProgressIndicator(), - ), - ), - ); - } - - if (error != null) { - return SliverToBoxAdapter( - child: Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Text( - 'Error loading albums: $error', - style: TextStyle( - color: context.colorScheme.error, - ), - ), - ), - ), - ); - } - if (albums.isEmpty) { return const SliverToBoxAdapter( child: Center( @@ -692,7 +633,7 @@ class _AlbumGrid extends StatelessWidget { } } -class _GridAlbumCard extends StatelessWidget { +class _GridAlbumCard extends ConsumerWidget { const _GridAlbumCard({ required this.album, required this.userId, @@ -702,11 +643,14 @@ class _GridAlbumCard extends StatelessWidget { final String? userId; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return GestureDetector( - onTap: () => context.router.push( - RemoteAlbumRoute(album: album), - ), + onTap: () { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); + context.router.push( + RemoteAlbumRoute(album: album), + ); + }, child: Card( elevation: 0, color: context.colorScheme.surfaceBright, diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index e06321413e..f6ba98f61c 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; @@ -202,6 +203,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { ); if (album != null) { + ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); context.replaceRoute( RemoteAlbumRoute(album: album), ); diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 552733980e..eba0a5ea81 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -6,15 +6,15 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; +import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -391,7 +391,8 @@ class _QuickAccessButtonList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final partners = ref.watch(partnerSharedWithProvider); + final partnerSharedWithAsync = ref.watch(driftSharedWithPartnerProvider); + final partners = partnerSharedWithAsync.valueOrNull ?? []; return SliverPadding( padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32), @@ -452,7 +453,6 @@ class _QuickAccessButtonList extends ConsumerWidget { fontWeight: FontWeight.w500, ), ), - // TODO: PIN code is needed onTap: () => context.pushRoute(const DriftLockedFolderRoute()), ), ListTile( @@ -466,7 +466,7 @@ class _QuickAccessButtonList extends ConsumerWidget { fontWeight: FontWeight.w500, ), ), - onTap: () => context.pushRoute(const PartnerRoute()), + onTap: () => context.pushRoute(const DriftPartnerRoute()), ), _PartnerList(partners: partners), ], @@ -480,7 +480,7 @@ class _QuickAccessButtonList extends ConsumerWidget { class _PartnerList extends StatelessWidget { const _PartnerList({required this.partners}); - final List partners; + final List partners; @override Widget build(BuildContext context) { @@ -503,7 +503,9 @@ class _PartnerList extends StatelessWidget { left: 12.0, right: 18.0, ), - leading: userAvatar(context, partner, radius: 16), + leading: PartnerUserAvatar( + partner: partner, + ), title: const Text( "partner_list_user_photos", style: TextStyle( diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index 9b42cdb103..e134b418e9 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -4,14 +4,45 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class DriftLockedFolderPage extends StatelessWidget { +class DriftLockedFolderPage extends ConsumerStatefulWidget { const DriftLockedFolderPage({super.key}); + @override + ConsumerState createState() => + _DriftLockedFolderPageState(); +} + +class _DriftLockedFolderPageState extends ConsumerState + with WidgetsBindingObserver { + bool _showOverlay = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (mounted) { + setState(() { + _showOverlay = state != AppLifecycleState.resumed; + }); + } + } + @override Widget build(BuildContext context) { return ProviderScope( @@ -30,12 +61,18 @@ class DriftLockedFolderPage extends StatelessWidget { }, ), ], - child: Timeline( - appBar: MesmerizingSliverAppBar( - title: 'locked_folder'.t(context: context), - ), - bottomSheet: const LockedFolderBottomSheet(), - ), + child: _showOverlay + ? const SizedBox() + : PopScope( + onPopInvokedWithResult: (didPop, _) => + didPop ? ref.read(authProvider.notifier).lockPinCode() : null, + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'locked_folder'.t(context: context), + ), + bottomSheet: const LockedFolderBottomSheet(), + ), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index baae893d39..6c77a480ea 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -6,11 +6,14 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftPartnerDetailPage extends StatelessWidget { - final UserDto partner; + final PartnerUserDto partner; const DriftPartnerDetailPage({ super.key, @@ -35,12 +38,7 @@ class DriftPartnerDetailPage extends StatelessWidget { title: partner.name, icon: Icons.person_outline, ), - topSliverWidget: _InfoBox( - onTap: () => { - // TODO: Create DriftUserProvider/DriftUserService to handle this action - }, - inTimeline: partner.inTimeline, - ), + topSliverWidget: _InfoBox(partner: partner), topSliverWidgetHeight: 110, bottomSheet: const PartnerDetailBottomSheet(), ), @@ -48,15 +46,53 @@ class DriftPartnerDetailPage extends StatelessWidget { } } -class _InfoBox extends StatelessWidget { - final VoidCallback onTap; - final bool inTimeline; +class _InfoBox extends ConsumerStatefulWidget { + final PartnerUserDto partner; const _InfoBox({ - required this.onTap, - required this.inTimeline, + required this.partner, }); + @override + ConsumerState<_InfoBox> createState() => _InfoBoxState(); +} + +class _InfoBoxState extends ConsumerState<_InfoBox> { + bool _inTimeline = false; + + @override + void initState() { + super.initState(); + _inTimeline = widget.partner.inTimeline; + } + + _toggleInTimeline() async { + final user = ref.read(currentUserProvider); + if (user == null) { + return; + } + + try { + await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline( + widget.partner.id, + user.id, + ); + + setState(() { + _inTimeline = !_inTimeline; + }); + } catch (error, stack) { + debugPrint("Failed to toggle in timeline: $error $stack"); + ImmichToast.show( + context: context, + toastType: ToastType.error, + durationInSecond: 1, + msg: "Failed to toggle the timeline setting", + ); + return; + } + } + @override Widget build(BuildContext context) { return SliverToBoxAdapter( @@ -96,8 +132,8 @@ class _InfoBox extends StatelessWidget { style: context.textTheme.bodyMedium, ), trailing: Switch( - value: inTimeline, - onChanged: (_) => onTap(), + value: _inTimeline, + onChanged: (_) => _toggleInTimeline(), ), ), ), diff --git a/mobile/lib/presentation/pages/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/drift_recently_taken.page.dart index e2972fad56..df303e7b31 100644 --- a/mobile/lib/presentation/pages/drift_recently_taken.page.dart +++ b/mobile/lib/presentation/pages/drift_recently_taken.page.dart @@ -1,9 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftRecentlyTakenPage extends StatelessWidget { @@ -29,7 +31,9 @@ class DriftRecentlyTakenPage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t()), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index bbfe6ddc74..6b68bfcd4e 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -1,17 +1,237 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/remote_album_sliver_app_bar.dart'; @RoutePage() -class RemoteAlbumPage extends StatelessWidget { +class RemoteAlbumPage extends ConsumerStatefulWidget { final RemoteAlbum album; - const RemoteAlbumPage({super.key, required this.album}); + const RemoteAlbumPage({ + super.key, + required this.album, + }); + + @override + ConsumerState createState() => _RemoteAlbumPageState(); +} + +class _RemoteAlbumPageState extends ConsumerState { + @override + void initState() { + super.initState(); + } + + Future addAssets(BuildContext context) async { + final albumAssets = + await ref.read(remoteAlbumProvider.notifier).getAssets(widget.album.id); + + final newAssets = await context.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: albumAssets.toSet(), + ), + ); + + if (newAssets == null || newAssets.isEmpty) { + return; + } + + final added = await ref.read(remoteAlbumProvider.notifier).addAssets( + widget.album.id, + newAssets.map((asset) { + final remoteAsset = asset as RemoteAsset; + return remoteAsset.id; + }).toList(), + ); + + if (added > 0) { + ImmichToast.show( + context: context, + msg: "assets_added_to_album_count".t( + context: context, + args: { + 'count': added.toString(), + }, + ), + toastType: ToastType.success, + ); + } + } + + Future addUsers(BuildContext context) async { + final newUsers = await context.pushRoute>( + DriftUserSelectionRoute(album: widget.album), + ); + + if (newUsers == null || newUsers.isEmpty) { + return; + } + + try { + await ref + .read(remoteAlbumProvider.notifier) + .addUsers(widget.album.id, newUsers); + + if (newUsers.isNotEmpty) { + ImmichToast.show( + context: context, + msg: "users_added_to_album_count".t( + context: context, + args: { + 'count': newUsers.length, + }, + ), + toastType: ToastType.success, + ); + } + + ref.invalidate(remoteAlbumSharedUsersProvider(widget.album.id)); + } catch (e) { + ImmichToast.show( + context: context, + msg: "Failed to add users to album: ${e.toString()}", + toastType: ToastType.error, + ); + } + } + + Future toggleAlbumOrder() async { + await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder( + widget.album.id, + ); + + ref.invalidate(timelineServiceProvider); + } + + Future deleteAlbum(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('delete_album'.t(context: context)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'album_delete_confirmation'.t( + context: context, + args: {'album': widget.album.name}, + ), + ), + const SizedBox(height: 8), + Text( + 'album_delete_confirmation_description'.t(context: context), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('cancel'.t(context: context)), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text('delete_album'.t(context: context)), + ), + ], + ); + }, + ); + + if (confirmed == true) { + try { + await ref + .read(remoteAlbumProvider.notifier) + .deleteAlbum(widget.album.id); + + ImmichToast.show( + context: context, + msg: 'library_deleted' + .t(context: context), // Using existing success message + toastType: ToastType.success, + ); + + context.pushRoute(const DriftAlbumsRoute()); + } catch (e) { + ImmichToast.show( + context: context, + msg: 'album_viewer_appbar_share_err_delete'.t(context: context), + toastType: ToastType.error, + ); + } + } + } + + Future showEditTitleAndDescription(BuildContext context) async { + final result = await showDialog<_EditAlbumData?>( + context: context, + barrierDismissible: true, + builder: (context) => _EditAlbumDialog(album: widget.album), + ); + + if (result != null && context.mounted) { + HapticFeedback.mediumImpact(); + } + } + + void showOptionSheet(BuildContext context) { + final user = ref.watch(currentUserProvider); + final isOwner = user != null ? user.id == widget.album.ownerId : false; + + showModalBottomSheet( + context: context, + backgroundColor: context.colorScheme.surface, + isScrollControlled: false, + builder: (context) { + return DriftRemoteAlbumOption( + onDeleteAlbum: isOwner + ? () async { + await deleteAlbum(context); + if (context.mounted) { + context.pop(); + } + } + : null, + onAddUsers: isOwner + ? () async { + await addUsers(context); + context.pop(); + } + : null, + onAddPhotos: () async { + await addAssets(context); + context.pop(); + }, + onToggleAlbumOrder: () async { + await toggleAlbumOrder(); + context.pop(); + }, + onEditAlbum: () async { + context.pop(); + await showEditTitleAndDescription(context); + }, + ); + }, + ); + } @override Widget build(BuildContext context) { @@ -21,19 +241,207 @@ class RemoteAlbumPage extends StatelessWidget { (ref) { final timelineService = ref .watch(timelineFactoryProvider) - .remoteAlbum(albumId: album.id); + .remoteAlbum(albumId: widget.album.id); ref.onDispose(timelineService.dispose); return timelineService; }, ), ], child: Timeline( - appBar: MesmerizingSliverAppBar( - title: album.name, + appBar: RemoteAlbumSliverAppBar( icon: Icons.photo_album_outlined, + onShowOptions: () => showOptionSheet(context), + onToggleAlbumOrder: () => toggleAlbumOrder(), + onEditTitle: () => showEditTitleAndDescription(context), ), bottomSheet: RemoteAlbumBottomSheet( - album: album, + album: widget.album, + ), + ), + ); + } +} + +class _EditAlbumData { + final String name; + final String? description; + + const _EditAlbumData({ + required this.name, + this.description, + }); +} + +class _EditAlbumDialog extends ConsumerStatefulWidget { + final RemoteAlbum album; + + const _EditAlbumDialog({ + required this.album, + }); + + @override + ConsumerState<_EditAlbumDialog> createState() => _EditAlbumDialogState(); +} + +class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { + late final TextEditingController titleController; + late final TextEditingController descriptionController; + final formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + titleController = TextEditingController(text: widget.album.name); + descriptionController = TextEditingController( + text: widget.album.description.isEmpty ? '' : widget.album.description, + ); + } + + @override + void dispose() { + titleController.dispose(); + descriptionController.dispose(); + super.dispose(); + } + + Future _handleSave() async { + if (formKey.currentState?.validate() != true) return; + + try { + final newTitle = titleController.text.trim(); + final newDescription = descriptionController.text.trim(); + + await ref.read(remoteAlbumProvider.notifier).updateAlbum( + widget.album.id, + name: newTitle, + description: newDescription.isEmpty ? null : newDescription, + ); + + if (mounted) { + Navigator.of(context).pop( + _EditAlbumData( + name: newTitle, + description: newDescription.isEmpty ? null : newDescription, + ), + ); + } + } catch (e) { + if (mounted) { + ImmichToast.show( + context: context, + msg: 'album_update_error'.t(context: context), + toastType: ToastType.error, + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: const EdgeInsets.all(24), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), + ), + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints(maxWidth: 550), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.edit_outlined, + color: context.colorScheme.primary, + size: 24, + ), + const SizedBox(width: 12), + Text( + 'edit_album'.t(context: context), + style: context.textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 24), + + // Album Name + Text( + 'album_name'.t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + TextFormField( + controller: titleController, + maxLines: 1, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + filled: true, + fillColor: context.colorScheme.surface, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'album_name_required'.t(context: context); + } + + return null; + }, + ), + const SizedBox(height: 18), + + // Description + Text( + 'description'.t(context: context).toUpperCase(), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + TextFormField( + controller: descriptionController, + maxLines: 4, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + filled: true, + fillColor: context.colorScheme.surface, + ), + ), + const SizedBox(height: 24), + + // Action Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: Text('cancel'.t(context: context)), + ), + const SizedBox(width: 12), + FilledButton( + onPressed: _handleSave, + child: Text('save'.t(context: context)), + ), + ], + ), + ], + ), + ), ), ), ); diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart new file mode 100644 index 0000000000..6f5c4c3e2b --- /dev/null +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -0,0 +1,215 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +// TODO: Refactor this provider when we have user provider/service/repository pattern in place +final driftUsersProvider = + FutureProvider.autoDispose>((ref) async { + final drift = ref.watch(driftProvider); + final currentUser = ref.watch(currentUserProvider); + + final userEntities = await drift.managers.userEntity.get(); + + final users = userEntities + .map( + (entity) => UserDto( + id: entity.id, + name: entity.name, + email: entity.email, + isAdmin: entity.isAdmin, + profileImagePath: entity.profileImagePath, + updatedAt: entity.updatedAt, + quotaSizeInBytes: entity.quotaSizeInBytes ?? 0, + quotaUsageInBytes: entity.quotaUsageInBytes, + isPartnerSharedBy: false, + isPartnerSharedWith: false, + avatarColor: AvatarColor.primary, + memoryEnabled: true, + inTimeline: true, + ), + ) + .toList(); + + users.removeWhere((u) => currentUser?.id == u.id); + + return users; +}); + +@RoutePage() +class DriftUserSelectionPage extends HookConsumerWidget { + final RemoteAlbum album; + + const DriftUserSelectionPage({ + super.key, + required this.album, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final AsyncValue> suggestedShareUsers = + ref.watch(driftUsersProvider); + final sharedUsersList = useState>({}); + + addNewUsersHandler() { + context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); + } + + buildTileIcon(UserDto user) { + if (sharedUsersList.value.contains(user)) { + return CircleAvatar( + backgroundColor: context.primaryColor, + child: const Icon( + Icons.check_rounded, + size: 25, + ), + ); + } else { + return UserCircleAvatar( + user: user, + ); + } + } + + buildUserList(List users) { + List usersChip = []; + + for (var user in sharedUsersList.value) { + usersChip.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Chip( + backgroundColor: context.primaryColor.withValues(alpha: 0.15), + label: Text( + user.name, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } + return ListView( + children: [ + Wrap( + children: [...usersChip], + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'suggestions'.tr(), + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ), + ListView.builder( + primary: false, + shrinkWrap: true, + itemBuilder: ((context, index) { + return ListTile( + leading: buildTileIcon(users[index]), + dense: true, + title: Text( + users[index].name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + users[index].email, + style: const TextStyle( + fontSize: 12, + ), + ), + onTap: () { + if (sharedUsersList.value.contains(users[index])) { + sharedUsersList.value = sharedUsersList.value + .where( + (selectedUser) => selectedUser.id != users[index].id, + ) + .toSet(); + } else { + sharedUsersList.value = { + ...sharedUsersList.value, + users[index], + }; + } + }, + ); + }), + itemCount: users.length, + ), + ], + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text( + 'invite_to_album', + ).tr(), + elevation: 0, + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + context.maybePop(null); + }, + ), + actions: [ + TextButton( + onPressed: + sharedUsersList.value.isEmpty ? null : addNewUsersHandler, + child: const Text( + "add", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ).tr(), + ), + ], + ), + body: suggestedShareUsers.widgetWhen( + onData: (users) { + // Get shared users for this album from the database + final sharedUsers = + ref.watch(remoteAlbumSharedUsersProvider(album.id)); + + return sharedUsers.when( + data: (albumSharedUsers) { + // Filter out users that are already shared with this album and the owner + final filteredUsers = users.where((user) { + return !albumSharedUsers + .any((sharedUser) => sharedUser.id == user.id) && + user.id != album.ownerId; + }).toList(); + + return buildUserList(filteredUsers); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) { + // If we can't load shared users, just filter out the owner + final filteredUsers = + users.where((user) => user.id != album.ownerId).toList(); + return buildUserList(filteredUsers); + }, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_video.page.dart b/mobile/lib/presentation/pages/drift_video.page.dart index 488d027177..8c0e8e6911 100644 --- a/mobile/lib/presentation/pages/drift_video.page.dart +++ b/mobile/lib/presentation/pages/drift_video.page.dart @@ -1,9 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftVideoPage extends StatelessWidget { @@ -27,7 +29,9 @@ class DriftVideoPage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar(title: 'videos'.t()), + ), ); } } diff --git a/mobile/lib/presentation/pages/local_timeline.page.dart b/mobile/lib/presentation/pages/local_timeline.page.dart index b4df0f64e2..fd4e44616b 100644 --- a/mobile/lib/presentation/pages/local_timeline.page.dart +++ b/mobile/lib/presentation/pages/local_timeline.page.dart @@ -30,6 +30,7 @@ class LocalTimelinePage extends StatelessWidget { child: Timeline( appBar: MesmerizingSliverAppBar(title: album.name), bottomSheet: const LocalAlbumBottomSheet(), + showStorageIndicator: true, ), ); } diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart new file mode 100644 index 0000000000..7101a42b01 --- /dev/null +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -0,0 +1,925 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart'; +import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart'; +import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart'; +import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart'; +import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart'; +import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; +import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; +import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; + +@RoutePage() +class DriftSearchPage extends HookConsumerWidget { + const DriftSearchPage({super.key, this.preFilter}); + + final SearchFilter? preFilter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textSearchType = useState(TextSearchType.context); + final searchHintText = + useState('sunrise_on_the_beach'.t(context: context)); + final textSearchController = useTextEditingController(); + final filter = useState( + SearchFilter( + people: preFilter?.people ?? {}, + location: preFilter?.location ?? SearchLocationFilter(), + camera: preFilter?.camera ?? SearchCameraFilter(), + date: preFilter?.date ?? SearchDateFilter(), + display: preFilter?.display ?? + SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: preFilter?.mediaType ?? AssetType.other, + language: + "${context.locale.languageCode}-${context.locale.countryCode}", + ), + ); + + final previousFilter = useState(null); + + final peopleCurrentFilterWidget = useState(null); + final dateRangeCurrentFilterWidget = useState(null); + final cameraCurrentFilterWidget = useState(null); + final locationCurrentFilterWidget = useState(null); + final mediaTypeCurrentFilterWidget = useState(null); + final displayOptionCurrentFilterWidget = useState(null); + + final isSearching = useState(false); + + SnackBar searchInfoSnackBar(String message) { + return SnackBar( + content: Text( + message, + style: context.textTheme.labelLarge, + ), + showCloseIcon: true, + behavior: SnackBarBehavior.fixed, + closeIconColor: context.colorScheme.onSurface, + ); + } + + search() async { + if (filter.value.isEmpty) { + return; + } + + if (preFilter == null && filter.value == previousFilter.value) { + return; + } + + isSearching.value = true; + ref.watch(paginatedSearchProvider.notifier).clear(); + final hasResult = await ref + .watch(paginatedSearchProvider.notifier) + .search(filter.value); + + if (!hasResult) { + context.showSnackBar( + searchInfoSnackBar('search_no_result'.t(context: context)), + ); + } + + previousFilter.value = filter.value; + isSearching.value = false; + } + + loadMoreSearchResult() async { + isSearching.value = true; + final hasResult = await ref + .watch(paginatedSearchProvider.notifier) + .search(filter.value); + + if (!hasResult) { + context.showSnackBar( + searchInfoSnackBar('search_no_more_result'.t(context: context)), + ); + } + + isSearching.value = false; + } + + searchPreFilter() { + if (preFilter != null) { + Future.delayed( + Duration.zero, + () { + search(); + + if (preFilter!.location.city != null) { + locationCurrentFilterWidget.value = Text( + preFilter!.location.city!, + style: context.textTheme.labelLarge, + ); + } + }, + ); + } + } + + useEffect( + () { + Future.microtask( + () => ref.invalidate(paginatedSearchProvider), + ); + searchPreFilter(); + + return null; + }, + [], + ); + + showPeoplePicker() { + handleOnSelect(Set value) { + filter.value = filter.value.copyWith( + people: value, + ); + + peopleCurrentFilterWidget.value = Text( + value + .map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)) + .join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + people: {}, + ); + + peopleCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_people_title'.t(context: context), + expanded: true, + onSearch: search, + onClear: handleClear, + child: PeoplePicker( + onSelect: handleOnSelect, + filter: filter.value.people, + ), + ), + ), + ); + } + + showLocationPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + location: SearchLocationFilter( + country: value['country'], + city: value['city'], + state: value['state'], + ), + ); + + final locationText = []; + if (value['country'] != null) { + locationText.add(value['country']!); + } + + if (value['state'] != null) { + locationText.add(value['state']!); + } + + if (value['city'] != null) { + locationText.add(value['city']!); + } + + locationCurrentFilterWidget.value = Text( + locationText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + location: SearchLocationFilter(), + ); + + locationCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_location_title'.t(context: context), + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + padding: EdgeInsets.only( + bottom: context.viewInsets.bottom, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LocationPicker( + onSelected: handleOnSelect, + filter: filter.value.location, + ), + ), + ), + ), + ), + ); + } + + showCameraPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter( + make: value['make'], + model: value['model'], + ), + ); + + cameraCurrentFilterWidget.value = Text( + '${value['make'] ?? ''} ${value['model'] ?? ''}', + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter(), + ); + + cameraCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_camera_title'.t(context: context), + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: CameraPicker( + onSelect: handleOnSelect, + filter: filter.value.camera, + ), + ), + ), + ); + } + + showDatePicker() async { + final firstDate = DateTime(1900); + final lastDate = DateTime.now(); + + final date = await showDateRangePicker( + context: context, + firstDate: firstDate, + lastDate: lastDate, + currentDate: DateTime.now(), + initialDateRange: DateTimeRange( + start: filter.value.date.takenAfter ?? lastDate, + end: filter.value.date.takenBefore ?? lastDate, + ), + helpText: 'search_filter_date_title'.t(context: context), + cancelText: 'cancel'.t(context: context), + confirmText: 'select'.t(context: context), + saveText: 'save'.t(context: context), + errorFormatText: 'invalid_date_format'.t(context: context), + errorInvalidText: 'invalid_date'.t(context: context), + fieldStartHintText: 'start_date'.t(context: context), + fieldEndHintText: 'end_date'.t(context: context), + initialEntryMode: DatePickerEntryMode.calendar, + keyboardType: TextInputType.text, + ); + + if (date == null) { + filter.value = filter.value.copyWith( + date: SearchDateFilter(), + ); + + dateRangeCurrentFilterWidget.value = null; + search(); + return; + } + + filter.value = filter.value.copyWith( + date: SearchDateFilter( + takenAfter: date.start, + takenBefore: date.end.add( + const Duration( + hours: 23, + minutes: 59, + seconds: 59, + ), + ), + ), + ); + + // If date range is less than 24 hours, set the end date to the end of the day + if (date.end.difference(date.start).inHours < 24) { + dateRangeCurrentFilterWidget.value = Text( + DateFormat.yMMMd().format(date.start.toLocal()), + style: context.textTheme.labelLarge, + ); + } else { + dateRangeCurrentFilterWidget.value = Text( + 'search_filter_date_interval'.t( + context: context, + args: { + "start": DateFormat.yMMMd().format(date.start.toLocal()), + "end": DateFormat.yMMMd().format(date.end.toLocal()), + }, + ), + style: context.textTheme.labelLarge, + ); + } + + search(); + } + + // MEDIA PICKER + showMediaTypePicker() { + handleOnSelected(AssetType assetType) { + filter.value = filter.value.copyWith( + mediaType: assetType, + ); + + mediaTypeCurrentFilterWidget.value = Text( + assetType == AssetType.image + ? 'image'.t(context: context) + : assetType == AssetType.video + ? 'video'.t(context: context) + : 'all'.t(context: context), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + mediaType: AssetType.other, + ); + + mediaTypeCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'search_filter_media_type_title'.t(context: context), + onSearch: search, + onClear: handleClear, + child: MediaTypePicker( + onSelect: handleOnSelected, + filter: filter.value.mediaType, + ), + ), + ); + } + + // DISPLAY OPTION + showDisplayOptionPicker() { + handleOnSelect(Map value) { + final filterText = []; + value.forEach((key, value) { + switch (key) { + case DisplayOption.notInAlbum: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isNotInAlbum: value, + ), + ); + if (value) { + filterText.add( + 'search_filter_display_option_not_in_album' + .t(context: context), + ); + } + break; + case DisplayOption.archive: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isArchive: value, + ), + ); + if (value) { + filterText.add('archive'.t(context: context)); + } + break; + case DisplayOption.favorite: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isFavorite: value, + ), + ); + if (value) { + filterText.add('favorite'.t(context: context)); + } + break; + } + }); + + if (filterText.isEmpty) { + displayOptionCurrentFilterWidget.value = null; + return; + } + + displayOptionCurrentFilterWidget.value = Text( + filterText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + ); + + displayOptionCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'display_options'.t(context: context), + onSearch: search, + onClear: handleClear, + child: DisplayOptionPicker( + onSelect: handleOnSelect, + filter: filter.value.display, + ), + ), + ); + } + + handleTextSubmitted(String value) { + switch (textSearchType.value) { + case TextSearchType.context: + filter.value = filter.value.copyWith( + filename: '', + context: value, + description: '', + ); + + break; + case TextSearchType.filename: + filter.value = filter.value.copyWith( + filename: value, + context: '', + description: '', + ); + + break; + case TextSearchType.description: + filter.value = filter.value.copyWith( + filename: '', + context: '', + description: value, + ); + break; + } + + search(); + } + + IconData getSearchPrefixIcon() => switch (textSearchType.value) { + TextSearchType.context => Icons.image_search_rounded, + TextSearchType.filename => Icons.abc_rounded, + TextSearchType.description => Icons.text_snippet_outlined, + }; + + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + automaticallyImplyLeading: true, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: MenuAnchor( + style: MenuStyle( + elevation: const WidgetStatePropertyAll(1), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(24), + ), + ), + ), + padding: const WidgetStatePropertyAll( + EdgeInsets.all(4), + ), + ), + builder: ( + BuildContext context, + MenuController controller, + Widget? child, + ) { + return IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.more_vert_rounded), + tooltip: 'Show text search menu', + ); + }, + menuChildren: [ + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.image_search_rounded), + title: Text( + 'search_by_context'.t(context: context), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: textSearchType.value == TextSearchType.context + ? context.colorScheme.primary + : null, + ), + ), + selectedColor: context.colorScheme.primary, + selected: textSearchType.value == TextSearchType.context, + ), + onPressed: () { + textSearchType.value = TextSearchType.context; + searchHintText.value = + 'sunrise_on_the_beach'.t(context: context); + }, + ), + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.abc_rounded), + title: Text( + 'search_filter_filename'.t(context: context), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: textSearchType.value == TextSearchType.filename + ? context.colorScheme.primary + : null, + ), + ), + selectedColor: context.colorScheme.primary, + selected: textSearchType.value == TextSearchType.filename, + ), + onPressed: () { + textSearchType.value = TextSearchType.filename; + searchHintText.value = + 'file_name_or_extension'.t(context: context); + }, + ), + MenuItemButton( + child: ListTile( + leading: const Icon(Icons.text_snippet_outlined), + title: Text( + 'search_by_description'.t(context: context), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: + textSearchType.value == TextSearchType.description + ? context.colorScheme.primary + : null, + ), + ), + selectedColor: context.colorScheme.primary, + selected: + textSearchType.value == TextSearchType.description, + ), + onPressed: () { + textSearchType.value = TextSearchType.description; + searchHintText.value = + 'search_by_description_example'.t(context: context); + }, + ), + ], + ), + ), + ], + title: Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(0), + width: 0, + ), + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withValues(alpha: 0.075), + context.colorScheme.primary.withValues(alpha: 0.09), + context.colorScheme.primary.withValues(alpha: 0.075), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SearchField( + hintText: searchHintText.value, + key: const Key('search_text_field'), + controller: textSearchController, + contentPadding: preFilter != null + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(8), + prefixIcon: preFilter != null + ? null + : Icon( + getSearchPrefixIcon(), + color: context.colorScheme.primary, + ), + onSubmitted: handleTextSubmitted, + focusNode: ref.watch(searchInputFocusProvider), + ), + ), + ), + body: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.only(top: 12.0), + sliver: SliverToBoxAdapter( + child: SizedBox( + height: 50, + child: ListView( + key: const Key('search_filter_chip_list'), + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SearchFilterChip( + icon: Icons.people_alt_outlined, + onTap: showPeoplePicker, + label: 'people'.t(context: context), + currentFilter: peopleCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.location_on_outlined, + onTap: showLocationPicker, + label: 'search_filter_location'.t(context: context), + currentFilter: locationCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.camera_alt_outlined, + onTap: showCameraPicker, + label: 'camera'.t(context: context), + currentFilter: cameraCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.date_range_outlined, + onTap: showDatePicker, + label: 'search_filter_date'.t(context: context), + currentFilter: dateRangeCurrentFilterWidget.value, + ), + SearchFilterChip( + key: const Key('media_type_chip'), + icon: Icons.video_collection_outlined, + onTap: showMediaTypePicker, + label: 'search_filter_media_type'.t(context: context), + currentFilter: mediaTypeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.display_settings_outlined, + onTap: showDisplayOptionPicker, + label: + 'search_filter_display_options'.t(context: context), + currentFilter: displayOptionCurrentFilterWidget.value, + ), + ], + ), + ), + ), + ), + if (isSearching.value) + const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), + ) + else + _SearchResultGrid(onScrollEnd: loadMoreSearchResult), + ], + ), + ); + } +} + +class _SearchResultGrid extends ConsumerWidget { + final VoidCallback onScrollEnd; + + const _SearchResultGrid({required this.onScrollEnd}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchResult = ref.watch(paginatedSearchProvider); + + if (searchResult.totalAssets == 0) { + return const _SearchEmptyContent(); + } + + return NotificationListener( + onNotification: (notification) { + final isBottomSheetNotification = notification.context + ?.findAncestorWidgetOfExactType() != + null; + + final metrics = notification.metrics; + final isVerticalScroll = metrics.axis == Axis.vertical; + + if (metrics.pixels >= metrics.maxScrollExtent && + isVerticalScroll && + !isBottomSheetNotification) { + onScrollEnd(); + } + + return true; + }, + child: SliverFillRemaining( + child: ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref + .watch(timelineFactoryProvider) + .fromAssets(searchResult.assets); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + key: ValueKey(searchResult.totalAssets), + appBar: null, + groupBy: GroupAssetsBy.none, + ), + ), + ), + ); + } +} + +class _SearchEmptyContent extends StatelessWidget { + const _SearchEmptyContent(); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: 40), + Center( + child: Image.asset( + context.isDarkTheme + ? 'assets/polaroid-dark.png' + : 'assets/polaroid-light.png', + height: 125, + ), + ), + const SizedBox(height: 16), + Center( + child: Text( + 'search_page_search_photos_videos'.t(context: context), + style: context.textTheme.labelLarge, + ), + ), + const SizedBox(height: 32), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: _QuickLinkList(), + ), + ], + ), + ); + } +} + +class _QuickLinkList extends StatelessWidget { + const _QuickLinkList(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + border: Border.all( + color: context.colorScheme.outline.withAlpha(10), + width: 1, + ), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(10), + context.colorScheme.primary.withAlpha(15), + context.colorScheme.primary.withAlpha(20), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + _QuickLink( + title: 'recently_taken'.t(context: context), + icon: Icons.schedule_outlined, + isTop: true, + onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()), + ), + _QuickLink( + title: 'videos'.t(context: context), + icon: Icons.play_circle_outline_rounded, + onTap: () => context.pushRoute(const DriftVideoRoute()), + ), + _QuickLink( + title: 'favorites'.t(context: context), + icon: Icons.favorite_border_rounded, + isBottom: true, + onTap: () => context.pushRoute(const DriftFavoriteRoute()), + ), + ], + ), + ); + } +} + +class _QuickLink extends StatelessWidget { + final String title; + final IconData icon; + final VoidCallback onTap; + final bool isTop; + final bool isBottom; + + const _QuickLink({ + required this.title, + required this.icon, + required this.onTap, + this.isTop = false, + this.isBottom = false, + }); + + @override + Widget build(BuildContext context) { + final borderRadius = BorderRadius.only( + topLeft: Radius.circular(isTop ? 20 : 0), + topRight: Radius.circular(isTop ? 20 : 0), + bottomLeft: Radius.circular(isBottom ? 20 : 0), + bottomRight: Radius.circular(isBottom ? 20 : 0), + ); + + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + ), + leading: Icon( + icon, + size: 26, + ), + title: Text( + title, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + onTap: onTap, + ); + } +} diff --git a/mobile/lib/presentation/pages/search/paginated_search.provider.dart b/mobile/lib/presentation/pages/search/paginated_search.provider.dart new file mode 100644 index 0000000000..84635fd0b9 --- /dev/null +++ b/mobile/lib/presentation/pages/search/paginated_search.provider.dart @@ -0,0 +1,40 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/search_result.model.dart'; +import 'package:immich_mobile/domain/services/search.service.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; + +final paginatedSearchProvider = + StateNotifierProvider( + (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), +); + +class PaginatedSearchNotifier extends StateNotifier { + final SearchService _searchService; + + PaginatedSearchNotifier(this._searchService) + : super(const SearchResult(assets: [], nextPage: 1)); + + Future search(SearchFilter filter) async { + if (state.nextPage == null) { + return false; + } + + final result = await _searchService.search(filter, state.nextPage!); + + if (result == null) { + return false; + } + + state = SearchResult( + assets: [...state.assets, ...result.assets], + nextPage: result.nextPage, + ); + + return true; + } + + clear() { + state = const SearchResult(assets: [], nextPage: 1); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index 94e3610a57..2ad285326c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -6,6 +6,7 @@ class BaseActionButton extends StatelessWidget { super.key, required this.label, required this.iconData, + this.iconColor, this.onPressed, this.onLongPressed, this.maxWidth = 90.0, @@ -15,6 +16,7 @@ class BaseActionButton extends StatelessWidget { final String label; final IconData iconData; + final Color? iconColor; final double maxWidth; final double? minWidth; final bool menuItem; @@ -27,7 +29,8 @@ class BaseActionButton extends StatelessWidget { minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); final iconTheme = IconTheme.of(context); final iconSize = iconTheme.size ?? 24.0; - final iconColor = iconTheme.color ?? context.themeData.iconTheme.color; + final iconColor = + this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color; final textColor = context.themeData.textTheme.labelLarge?.color; if (menuItem) { diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart new file mode 100644 index 0000000000..2900d55834 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; + +class CastActionButton extends ConsumerWidget { + const CastActionButton({super.key, this.menuItem = true}); + + final bool menuItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + + return BaseActionButton( + iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + iconColor: + isCasting ? context.primaryColor : null, // null = default color + label: "cast".t(context: context), + onPressed: () { + showDialog( + context: context, + builder: (context) => const CastDialog(), + ); + }, + menuItem: menuItem, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index dfc84b4190..90534ca68c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -1,10 +1,42 @@ import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DeleteLocalActionButton extends ConsumerWidget { - const DeleteLocalActionButton({super.key}); + final ActionSource source; + + const DeleteLocalActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).deleteLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'delete_local_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } @override Widget build(BuildContext context, WidgetRef ref) { @@ -12,6 +44,7 @@ class DeleteLocalActionButton extends ConsumerWidget { maxWidth: 95.0, iconData: Icons.no_cell_outlined, label: "control_bottom_app_bar_delete_from_local".t(context: context), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart index 53ea5d4946..c6eda703a5 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart @@ -1,16 +1,54 @@ +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DownloadActionButton extends ConsumerWidget { - const DownloadActionButton({super.key}); + final ActionSource source; + + const DownloadActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).downloadAll(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } else if (result.count > 0) { + ImmichToast.show( + context: context, + msg: 'download_action_prompt' + .t(context: context, args: {'count': result.count.toString()}), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.success, + ); + } + } @override Widget build(BuildContext context, WidgetRef ref) { return BaseActionButton( iconData: Icons.download, label: "download".t(context: context), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 6a8864c14f..1b1553dabc 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -1,12 +1,49 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class ShareActionButton extends ConsumerWidget { - const ShareActionButton({super.key}); + final ActionSource source; + + const ShareActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).shareAssets(source); + ref.read(multiSelectProvider.notifier).reset(); + + if (!context.mounted) { + return; + } + + if (!result.success) { + ImmichToast.show( + context: context, + msg: 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } else if (result.count > 0) { + ImmichToast.show( + context: context, + msg: 'share_action_prompt' + .t(context: context, args: {'count': result.count.toString()}), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.success, + ); + } + } @override Widget build(BuildContext context, WidgetRef ref) { @@ -14,6 +51,7 @@ class ShareActionButton extends ConsumerWidget { iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, label: 'share'.t(context: context), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart index dc42eb96f1..13782c0098 100644 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart @@ -1,16 +1,56 @@ import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class StackActionButton extends ConsumerWidget { - const StackActionButton({super.key}); + final ActionSource source; + + const StackActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access stack action'); + } + + final result = + await ref.read(actionProvider.notifier).stack(user.id, source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'stack_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } @override Widget build(BuildContext context, WidgetRef ref) { return BaseActionButton( iconData: Icons.filter_none_rounded, label: "stack".t(context: context), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart new file mode 100644 index 0000000000..c2757043a3 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class UnStackActionButton extends ConsumerWidget { + final ActionSource source; + + const UnStackActionButton({super.key, required this.source}); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).unStack(source); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'unstack_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.filter_none_rounded, + label: "unstack".t(context: context), + onPressed: () => _onTap(context, ref), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart new file mode 100644 index 0000000000..cb4e02b56c --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart @@ -0,0 +1,24 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; + +class StackChildrenNotifier + extends AutoDisposeFamilyAsyncNotifier, BaseAsset?> { + @override + Future> build(BaseAsset? asset) async { + if (asset == null || + asset is! RemoteAsset || + asset.stackId == null || + // The stackCount check is to ensure we only fetch stacks for timelines that have stacks + asset.stackCount == 0) { + return const []; + } + + return ref.watch(assetServiceProvider).getStack(asset); + } +} + +final stackChildrenNotifier = AsyncNotifierProvider.autoDispose + .family, BaseAsset?>( + StackChildrenNotifier.new, +); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart new file mode 100644 index 0000000000..8b3d0c6575 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; + +class AssetStackRow extends ConsumerWidget { + const AssetStackRow({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = + ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: ref.watch(stackChildrenNotifier(asset)).when( + data: (state) => SizedBox.square( + dimension: 80, + child: _StackList(stack: state), + ), + error: (_, __) => const SizedBox.shrink(), + loading: () => const SizedBox.shrink(), + ), + ), + ); + } +} + +class _StackList extends ConsumerWidget { + final List stack; + + const _StackList({required this.stack}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only( + left: 5, + right: 5, + bottom: 30, + ), + itemCount: stack.length, + itemBuilder: (ctx, index) { + final asset = stack[index]; + return Padding( + padding: const EdgeInsets.only(right: 5), + child: GestureDetector( + onTap: () { + ref.read(assetViewerProvider.notifier).setStackIndex(index); + ref.read(currentAssetNotifier.notifier).setAsset(asset); + }, + child: Container( + height: 60, + width: 60, + decoration: index == + ref.watch(assetViewerProvider.select((s) => s.stackIndex)) + ? const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: Border.fromBorderSide( + BorderSide(color: Colors.white, width: 2), + ), + ) + : const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6)), + border: null, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Stack( + fit: StackFit.expand, + children: [ + Image( + fit: BoxFit.cover, + image: getThumbnailImageProvider( + remoteId: asset.id, + size: const Size.square(60), + ), + ), + if (asset.isVideo) + const Icon( + Icons.play_circle_outline_rounded, + color: Colors.white, + size: 16, + shadows: [ + Shadow( + blurRadius: 5.0, + color: Color.fromRGBO(0, 0, 0, 0.6), + offset: Offset(0.0, 0.0), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index dfc0023685..9356c2f43e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -1,13 +1,17 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; @@ -18,8 +22,10 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart' import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; import 'package:platform/platform.dart'; @@ -83,6 +89,7 @@ class _AssetViewerState extends ConsumerState { double previousExtent = _kBottomSheetMinimumExtent; Offset dragDownPosition = Offset.zero; int totalAssets = 0; + int stackIndex = 0; BuildContext? scaffoldContext; Map videoPlayerKeys = {}; @@ -165,6 +172,10 @@ class _AssetViewerState extends ConsumerState { void _onAssetChanged(int index) { final asset = ref.read(timelineServiceProvider).getAsset(index); + // Always holds the current asset from the timeline + ref.read(assetViewerProvider.notifier).setAsset(asset); + // The currentAssetNotifier actually holds the current asset that is displayed + // which could be stack children as well ref.read(currentAssetNotifier.notifier).setAsset(asset); if (asset.isVideo || asset.isMotionPhoto) { ref.read(videoPlaybackValueProvider.notifier).reset(); @@ -184,6 +195,40 @@ class _AssetViewerState extends ConsumerState { } }); _delayedOperations.add(timer); + + _handleCasting(asset); + } + + void _handleCasting(BaseAsset asset) { + if (!ref.read(castProvider).isCasting) return; + + // hide any casting snackbars if they exist + context.scaffoldMessenger.hideCurrentSnackBar(); + + // send image to casting if the server has it + if (asset.hasRemote) { + final remoteAsset = asset as RemoteAsset; + + ref.read(castProvider.notifier).loadMedia(remoteAsset, false); + } else { + // casting cannot show local assets + context.scaffoldMessenger.clearSnackBars(); + + if (ref.read(castProvider).isCasting) { + ref.read(castProvider.notifier).stop(); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + "local_asset_cast_failed".tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), + ); + } + } } void _onPageBuild(PhotoViewControllerBase controller) { @@ -452,7 +497,12 @@ class _AssetViewerState extends ConsumerState { ImageChunkEvent? progress, int index, ) { - final asset = ref.read(timelineServiceProvider).getAsset(index); + BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); + final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren != null && stackChildren.isNotEmpty) { + asset = stackChildren + .elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + } return Container( width: double.infinity, height: double.infinity, @@ -480,9 +530,14 @@ class _AssetViewerState extends ConsumerState { PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) { scaffoldContext ??= ctx; - final asset = ref.read(timelineServiceProvider).getAsset(index); - final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); + BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); + final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren != null && stackChildren.isNotEmpty) { + asset = stackChildren + .elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); + } + final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); if (asset.isImage && !isPlayingMotionVideo) { return _imageBuilder(ctx, asset); } @@ -568,8 +623,24 @@ class _AssetViewerState extends ConsumerState { // Using multiple selectors to avoid unnecessary rebuilds for other state changes ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); + ref.watch(assetViewerProvider.select((s) => s.stackIndex)); ref.watch(isPlayingMotionVideoProvider); + // Listen for casting changes and send initial asset to the cast provider + ref.listen(castProvider.select((value) => value.isCasting), + (_, isCasting) async { + if (!isCasting) return; + + final asset = ref.read(currentAssetNotifier); + if (asset == null) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + _handleCasting(asset); + }); + }); + + final isInLockedView = ref.watch(inLockedViewProvider); + // Currently it is not possible to scroll the asset when the bottom sheet is open all the way. // Issue: https://github.com/flutter/flutter/issues/109037 // TODO: Add a custom scrum builder once the fix lands on stable @@ -596,7 +667,17 @@ class _AssetViewerState extends ConsumerState { backgroundDecoration: BoxDecoration(color: backgroundColor), enablePanAlways: true, ), - bottomNavigationBar: const ViewerBottomBar(), + bottomNavigationBar: showingBottomSheet + ? const SizedBox.shrink() + : Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AssetStackRow(), + if (!isInLockedView) const ViewerBottomBar(), + ], + ), ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart index 020d1d9b2c..825b637e8d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -1,26 +1,40 @@ +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +class ViewerOpenBottomSheetEvent extends Event { + const ViewerOpenBottomSheetEvent(); +} + class AssetViewerState { final int backgroundOpacity; final bool showingBottomSheet; final bool showingControls; + final BaseAsset? currentAsset; + final int stackIndex; const AssetViewerState({ this.backgroundOpacity = 255, this.showingBottomSheet = false, this.showingControls = true, + this.currentAsset, + this.stackIndex = 0, }); AssetViewerState copyWith({ int? backgroundOpacity, bool? showingBottomSheet, bool? showingControls, + BaseAsset? currentAsset, + int? stackIndex, }) { return AssetViewerState( backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet, showingControls: showingControls ?? this.showingControls, + currentAsset: currentAsset ?? this.currentAsset, + stackIndex: stackIndex ?? this.stackIndex, ); } @@ -36,14 +50,18 @@ class AssetViewerState { return other is AssetViewerState && other.backgroundOpacity == backgroundOpacity && other.showingBottomSheet == showingBottomSheet && - other.showingControls == showingControls; + other.showingControls == showingControls && + other.currentAsset == currentAsset && + other.stackIndex == stackIndex; } @override int get hashCode => backgroundOpacity.hashCode ^ showingBottomSheet.hashCode ^ - showingControls.hashCode; + showingControls.hashCode ^ + currentAsset.hashCode ^ + stackIndex.hashCode; } class AssetViewerStateNotifier extends AutoDisposeNotifier { @@ -52,6 +70,10 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier { return const AssetViewerState(); } + void setAsset(BaseAsset? asset) { + state = state.copyWith(currentAsset: asset, stackIndex: 0); + } + void setOpacity(int opacity) { state = state.copyWith( backgroundOpacity: opacity, @@ -76,6 +98,10 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier { void toggleControls() { state = state.copyWith(showingControls: !state.showingControls); } + + void setStackIndex(int index) { + state = state.copyWith(stackIndex: index); + } } final assetViewerProvider = diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index a28d5eafa4..d35a315f48 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -3,9 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; @@ -38,8 +36,7 @@ class ViewerBottomBar extends ConsumerWidget { } final actions = [ - const ShareActionButton(), - const _EditActionButton(), + const ShareActionButton(source: ActionSource.viewer), if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer), ]; @@ -86,15 +83,3 @@ class ViewerBottomBar extends ConsumerWidget { ); } } - -class _EditActionButton extends ConsumerWidget { - const _EditActionButton(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.tune_outlined, - label: 'edit'.t(context: context), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index a7a2a57ce5..89822fef91 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_ import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -44,12 +45,15 @@ class AssetDetailBottomSheet extends ConsumerWidget { serverInfoProvider.select((state) => state.serverFeatures.trash), ); + final isInLockedView = ref.watch(inLockedViewProvider); + final actions = [ - const ShareActionButton(), + const ShareActionButton(source: ActionSource.viewer), if (asset.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.viewer), const ArchiveActionButton(source: ActionSource.viewer), - if (!asset.hasLocal) const DownloadActionButton(), + if (!asset.hasLocal) + const DownloadActionButton(source: ActionSource.viewer), isTrashEnable ? const TrashActionButton(source: ActionSource.viewer) : const DeletePermanentActionButton(source: ActionSource.viewer), @@ -58,13 +62,15 @@ class AssetDetailBottomSheet extends ConsumerWidget { ), ], if (asset.storage == AssetState.local) ...[ - const DeleteLocalActionButton(), + const DeleteLocalActionButton(source: ActionSource.viewer), const UploadActionButton(), ], ]; + final lockedViewActions = []; + return BaseBottomSheet( - actions: actions, + actions: isInLockedView ? lockedViewActions : actions, slivers: const [_AssetDetailBottomSheet()], controller: controller, initialChildSize: initialChildSize, @@ -73,6 +79,7 @@ class AssetDetailBottomSheet extends ConsumerWidget { expand: false, shouldCloseOnMinExtent: false, resizeOnScroll: false, + backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, ); } } @@ -84,14 +91,18 @@ class _AssetDetailBottomSheet extends ConsumerWidget { final dateTime = asset.createdAt.toLocal(); final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); - return '$date$_kSeparator$time'; + final timezone = dateTime.timeZoneOffset.isNegative + ? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}' + : 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'; + return '$date$_kSeparator$time $timezone'; } String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { final height = asset.height ?? exifInfo?.height; final width = asset.width ?? exifInfo?.width; - final resolution = - (width != null && height != null) ? "$width x $height" : null; + final resolution = (width != null && height != null) + ? "${width.toInt()} x ${height.toInt()}" + : null; final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null; @@ -150,46 +161,46 @@ class _AssetDetailBottomSheet extends ConsumerWidget { // Asset Date and Time _SheetTile( title: _getDateTime(context, asset), - titleStyle: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 16, + titleStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, ), ), const SheetLocationDetails(), // Details header _SheetTile( title: 'exif_bottom_sheet_details'.t(context: context), - titleStyle: context.textTheme.labelLarge, + titleStyle: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), ), // File info _SheetTile( title: asset.name, - titleStyle: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.w600), + titleStyle: context.textTheme.labelLarge, leading: Icon( asset.isImage ? Icons.image_outlined : Icons.videocam_outlined, - size: 30, + size: 24, color: context.textTheme.labelLarge?.color, ), subtitle: _getFileInfo(asset, exifInfo), - subtitleStyle: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(200), + subtitleStyle: context.textTheme.bodyMedium?.copyWith( + color: context.textTheme.bodyMedium?.color?.withAlpha(155), ), ), // Camera info if (cameraTitle != null) _SheetTile( title: cameraTitle, - titleStyle: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.w600), + titleStyle: context.textTheme.labelLarge, leading: Icon( Icons.camera_outlined, - size: 30, + size: 24, color: context.textTheme.labelLarge?.color, ), subtitle: _getCameraInfoSubtitle(exifInfo), - subtitleStyle: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(200), + subtitleStyle: context.textTheme.bodyMedium?.copyWith( + color: context.textTheme.bodyMedium?.color?.withAlpha(155), ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart index 2d22d063bd..855328ebde 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart @@ -72,7 +72,7 @@ class _SheetLocationDetailsState extends ConsumerState { // Guard no lat/lng if (!hasCoordinates || - (asset is LocalAsset && !(asset as LocalAsset).hasRemote)) { + (asset != null && asset is LocalAsset && asset!.hasRemote)) { return const SizedBox.shrink(); } @@ -95,7 +95,10 @@ class _SheetLocationDetailsState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16), child: Text( "exif_bottom_sheet_location".t(context: context), - style: context.textTheme.labelLarge, + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), ), ), ExifMap( @@ -109,15 +112,13 @@ class _SheetLocationDetailsState extends ConsumerState { padding: const EdgeInsets.only(bottom: 4.0), child: Text( locationName, - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), + style: context.textTheme.labelLarge, ), ), Text( coordinates, - style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(150), + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index 0f3d46f673..4cdf9f2287 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -5,12 +5,16 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { const ViewerTopAppBar({super.key}); @@ -24,6 +28,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; + final isInLockedView = ref.watch(inLockedViewProvider); final isShowingSheet = ref .watch(assetViewerProvider.select((state) => state.showingBottomSheet)); @@ -37,7 +42,17 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { opacity = 0; } + final isCasting = ref.watch( + castProvider.select((c) => c.isCasting), + ); + final websocketConnected = + ref.watch(websocketProvider.select((c) => c.isConnected)); + final actions = [ + if (isCasting || (asset.hasRemote && websocketConnected)) + const CastActionButton( + menuItem: true, + ), if (asset.hasRemote && isOwner && !asset.isFavorite) const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), if (asset.hasRemote && isOwner && asset.isFavorite) @@ -49,6 +64,14 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { const _KebabMenu(), ]; + final lockedViewActions = [ + if (isCasting || (asset.hasRemote && websocketConnected)) + const CastActionButton( + menuItem: true, + ), + const _KebabMenu(), + ]; + return IgnorePointer( ignoring: opacity < 255, child: AnimatedOpacity( @@ -61,7 +84,11 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { iconTheme: const IconThemeData(size: 22, color: Colors.white), actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), shape: const Border(), - actions: isShowingSheet ? null : actions, + actions: isShowingSheet + ? null + : isInLockedView + ? lockedViewActions + : actions, ), ), ); diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart new file mode 100644 index 0000000000..c817b9b4b6 --- /dev/null +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +class BackupToggleButton extends ConsumerStatefulWidget { + final VoidCallback onStart; + final VoidCallback onStop; + + const BackupToggleButton({ + super.key, + required this.onStart, + required this.onStop, + }); + + @override + ConsumerState createState() => BackupToggleButtonState(); +} + +class BackupToggleButtonState extends ConsumerState + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _gradientAnimation; + bool _isEnabled = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(seconds: 8), + vsync: this, + ); + + _gradientAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _isEnabled = ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableBackup); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _onToggle(bool value) async { + await ref + .read(appSettingsServiceProvider) + .setSetting(AppSettingsEnum.enableBackup, value); + + setState(() { + _isEnabled = value; + }); + + if (value) { + widget.onStart.call(); + } else { + widget.onStop.call(); + } + } + + @override + Widget build(BuildContext context) { + final enqueueCount = ref.watch( + driftBackupProvider.select((state) => state.enqueueCount), + ); + + final enqueueTotalCount = ref.watch( + driftBackupProvider.select((state) => state.enqueueTotalCount), + ); + + final isCanceling = ref.watch( + driftBackupProvider.select((state) => state.isCanceling), + ); + + final uploadTasks = ref.watch( + driftBackupProvider.select((state) => state.uploadItems), + ); + + final isUploading = uploadTasks.isNotEmpty; + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final gradientColors = [ + Color.lerp( + context.primaryColor.withValues(alpha: 0.5), + context.primaryColor.withValues(alpha: 0.3), + _gradientAnimation.value, + )!, + Color.lerp( + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.4), + _gradientAnimation.value, + )!, + Color.lerp( + context.primaryColor.withValues(alpha: 0.3), + context.primaryColor.withValues(alpha: 0.5), + _gradientAnimation.value, + )!, + ]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + colors: gradientColors, + stops: const [0.0, 0.5, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: context.primaryColor.withValues(alpha: 0.1), + blurRadius: 12, + offset: const Offset(0, 2), + ), + ], + ), + child: Container( + margin: const EdgeInsets.all(1.5), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(18.5)), + color: context.colorScheme.surfaceContainerLow, + ), + child: Material( + color: context.colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.all(Radius.circular(20.5)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(20.5)), + onTap: () => isCanceling ? null : _onToggle(!_isEnabled), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.1), + ], + ), + ), + child: isUploading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Icon( + Icons.cloud_upload_outlined, + color: context.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "enable_backup".t(context: context), + style: + context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ], + ), + if (enqueueCount != enqueueTotalCount) + Text( + "queue_status".t( + context: context, + args: { + 'count': enqueueCount.toString(), + 'total': enqueueTotalCount.toString(), + }, + ), + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), + if (isCanceling) + Row( + children: [ + Text( + "canceling".t(), + style: context.textTheme.labelLarge, + ), + const SizedBox(width: 4), + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + backgroundColor: context + .colorScheme.onSurface + .withValues(alpha: 0.2), + ), + ), + ], + ), + ], + ), + ), + Switch.adaptive( + value: _isEnabled, + onChanged: (value) => + isCanceling ? null : _onToggle(value), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index c23f268465..9ed35da4cd 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -33,12 +33,12 @@ class ArchiveBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(), + const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const UnArchiveActionButton(source: ActionSource.timeline), const FavoriteActionButton(source: ActionSource.timeline), - const DownloadActionButton(), + const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton( @@ -49,10 +49,10 @@ class ArchiveBottomSheet extends ConsumerWidget { const MoveToLockFolderActionButton( source: ActionSource.timeline, ), - const StackActionButton(), + const StackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ - const DeleteLocalActionButton(), + const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(), ], ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart index 2db8ae2b4c..e172eec03b 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart @@ -14,6 +14,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget { final bool expand; final bool shouldCloseOnMinExtent; final bool resizeOnScroll; + final Color? backgroundColor; const BaseBottomSheet({ super.key, @@ -26,6 +27,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget { this.expand = true, this.shouldCloseOnMinExtent = true, this.resizeOnScroll = true, + this.backgroundColor, }); @override @@ -69,8 +71,8 @@ class _BaseDraggableScrollableSheetState shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent, builder: (BuildContext context, ScrollController scrollController) { return Card( - color: context.colorScheme.surfaceContainerHigh, - surfaceTintColor: context.colorScheme.surfaceContainerHigh, + color: widget.backgroundColor ?? + context.colorScheme.surfaceContainerHigh, borderOnForeground: false, clipBehavior: Clip.antiAlias, elevation: 6.0, diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 2f8208a80b..a1e1255a9f 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -33,12 +33,12 @@ class FavoriteBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(), + const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const UnFavoriteActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline), - const DownloadActionButton(), + const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton( @@ -49,10 +49,10 @@ class FavoriteBottomSheet extends ConsumerWidget { const MoveToLockFolderActionButton( source: ActionSource.timeline, ), - const StackActionButton(), + const StackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ - const DeleteLocalActionButton(), + const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(), ], ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 900adefd0b..373d264d82 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -33,12 +33,12 @@ class GeneralBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(), + const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline), const FavoriteActionButton(source: ActionSource.timeline), - const DownloadActionButton(), + const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton( @@ -49,10 +49,10 @@ class GeneralBottomSheet extends ConsumerWidget { const MoveToLockFolderActionButton( source: ActionSource.timeline, ), - const StackActionButton(), + const StackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ - const DeleteLocalActionButton(), + const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(), ], ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 3fd717f516..2ad0fe7485 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; @@ -15,8 +16,8 @@ class LocalAlbumBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - ShareActionButton(), - DeleteLocalActionButton(), + ShareActionButton(source: ActionSource.timeline), + DeleteLocalActionButton(source: ActionSource.timeline), UploadActionButton(), ], ); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index 97b2646f32..7f82f750f7 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -17,8 +17,8 @@ class LockedFolderBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - ShareActionButton(), - DownloadActionButton(), + ShareActionButton(source: ActionSource.timeline), + DownloadActionButton(source: ActionSource.timeline), DeletePermanentActionButton(source: ActionSource.timeline), RemoveFromLockFolderActionButton(source: ActionSource.timeline), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index 7af8ab7c86..5e4dae34bc 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -14,8 +15,8 @@ class PartnerDetailBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - ShareActionButton(), - DownloadActionButton(), + ShareActionButton(source: ActionSource.timeline), + DownloadActionButton(source: ActionSource.timeline), ], ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 0268a2b386..cfb6fe4f1a 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -36,12 +36,12 @@ class RemoteAlbumBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(), + const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline), const FavoriteActionButton(source: ActionSource.timeline), - const DownloadActionButton(), + const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton( @@ -52,10 +52,10 @@ class RemoteAlbumBottomSheet extends ConsumerWidget { const MoveToLockFolderActionButton( source: ActionSource.timeline, ), - const StackActionButton(), + const StackActionButton(source: ActionSource.timeline), ], if (multiselect.hasLocal) ...[ - const DeleteLocalActionButton(), + const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(), ], RemoveFromAlbumActionButton( diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e79665baf7..970bb581cf 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -12,7 +12,13 @@ ImageProvider getFullImageProvider( // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { - provider = LocalFullImageProvider(asset: asset as LocalAsset, size: size); + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + provider = LocalFullImageProvider( + id: id, + name: asset.name, + size: size, + type: asset.type, + ); } else { final String assetId; if (asset is LocalAsset && asset.hasRemote) { @@ -43,7 +49,13 @@ ImageProvider getThumbnailImageProvider({ } if (_shouldUseLocalAsset(asset!)) { - return LocalThumbProvider(asset: asset as LocalAsset, size: size); + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + return LocalThumbProvider( + id: id, + updatedAt: asset.updatedAt, + name: asset.name, + size: size, + ); } final String assetId; @@ -59,5 +71,5 @@ ImageProvider getThumbnailImageProvider({ } bool _shouldUseLocalAsset(BaseAsset asset) => - asset is LocalAsset && + asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index f046fcad47..65311de48a 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -21,11 +21,15 @@ class LocalThumbProvider extends ImageProvider { const AssetMediaRepository(); final CacheManager? cacheManager; - final LocalAsset asset; + final String id; + final DateTime updatedAt; + final String name; final Size size; const LocalThumbProvider({ - required this.asset, + required this.id, + required this.updatedAt, + required this.name, this.size = const Size.square(kTimelineFixedTileExtent), this.cacheManager, }); @@ -46,7 +50,10 @@ class LocalThumbProvider extends ImageProvider { scale: 1.0, informationCollector: () => [ DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Asset', key.asset), + DiagnosticsProperty('Id', key.id), + DiagnosticsProperty('Updated at', key.updatedAt), + DiagnosticsProperty('Name', key.name), + DiagnosticsProperty('Size', key.size), ], ); } @@ -57,7 +64,7 @@ class LocalThumbProvider extends ImageProvider { ImageDecoderCallback decode, ) async { final cacheKey = - '${key.asset.id}-${key.asset.updatedAt}-${key.size.width}x${key.size.height}'; + '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}'; final fileFromCache = await cache.getFileFromCache(cacheKey); if (fileFromCache != null) { @@ -69,11 +76,11 @@ class LocalThumbProvider extends ImageProvider { } final thumbnailBytes = - await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); + await _assetMediaRepository.getThumbnail(key.id, size: key.size); if (thumbnailBytes == null) { PaintingBinding.instance.imageCache.evict(key); throw StateError( - "Loading thumb for local photo ${key.asset.name} failed", + "Loading thumb for local photo ${key.name} failed", ); } @@ -86,14 +93,13 @@ class LocalThumbProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalThumbProvider) { - return asset.id == other.asset.id && - asset.updatedAt == other.asset.updatedAt; + return id == other.id && updatedAt == other.updatedAt; } return false; } @override - int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode; + int get hashCode => id.hashCode ^ updatedAt.hashCode; } class LocalFullImageProvider extends ImageProvider { @@ -101,12 +107,16 @@ class LocalFullImageProvider extends ImageProvider { const AssetMediaRepository(); final StorageRepository _storageRepository = const StorageRepository(); - final LocalAsset asset; + final String id; + final String name; final Size size; + final AssetType type; const LocalFullImageProvider({ - required this.asset, + required this.id, + required this.name, required this.size, + required this.type, }); @override @@ -123,7 +133,7 @@ class LocalFullImageProvider extends ImageProvider { codec: _codec(key, decode), scale: 1.0, informationCollector: () sync* { - yield ErrorDescription(asset.name); + yield ErrorDescription(name); }, ); } @@ -134,24 +144,24 @@ class LocalFullImageProvider extends ImageProvider { ImageDecoderCallback decode, ) async* { try { - switch (key.asset.type) { + switch (key.type) { case AssetType.image: yield* _decodeProgressive(key, decode); break; case AssetType.video: final codec = await _getThumbnailCodec(key, decode); if (codec == null) { - throw StateError("Failed to load preview for ${key.asset.name}"); + throw StateError("Failed to load preview for ${key.name}"); } yield codec; break; case AssetType.other: case AssetType.audio: - throw StateError('Unsupported asset type ${key.asset.type}'); + throw StateError('Unsupported asset type ${key.type}'); } } catch (error, stack) { Logger('ImmichLocalImageProvider') - .severe('Error loading local image ${key.asset.name}', error, stack); + .severe('Error loading local image ${key.name}', error, stack); throw const ImageLoadingException( 'Could not load image from local storage', ); @@ -163,7 +173,7 @@ class LocalFullImageProvider extends ImageProvider { ImageDecoderCallback decode, ) async { final thumbBytes = - await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); + await _assetMediaRepository.getThumbnail(key.id, size: key.size); if (thumbBytes == null) { return null; } @@ -175,9 +185,9 @@ class LocalFullImageProvider extends ImageProvider { LocalFullImageProvider key, ImageDecoderCallback decode, ) async* { - final file = await _storageRepository.getFileForAsset(key.asset.id); + final file = await _storageRepository.getFileForAsset(key.id); if (file == null) { - throw StateError("Opening file for asset ${key.asset.name} failed"); + throw StateError("Opening file for asset ${key.name} failed"); } final fileSize = await file.length(); @@ -195,7 +205,7 @@ class LocalFullImageProvider extends ImageProvider { (key.size.height * progressiveMultiplier).clamp(256, 1024), ); final mediumThumb = - await _assetMediaRepository.getThumbnail(key.asset.id, size: size); + await _assetMediaRepository.getThumbnail(key.id, size: size); if (mediumThumb != null) { final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb); yield await decode(mediumBuffer); @@ -212,7 +222,7 @@ class LocalFullImageProvider extends ImageProvider { (key.size.height * progressiveMultiplier).clamp(512, 2048), ); final highThumb = - await _assetMediaRepository.getThumbnail(key.asset.id, size: size); + await _assetMediaRepository.getThumbnail(key.id, size: size); if (highThumb != null) { final highBuffer = await ImmutableBuffer.fromUint8List(highThumb); yield await decode(highBuffer); @@ -228,14 +238,15 @@ class LocalFullImageProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalFullImageProvider) { - return asset.id == other.asset.id && - asset.updatedAt == other.asset.updatedAt && - size == other.size; + return id == other.id && + size == other.size && + type == other.type && + name == other.name; } return false; } @override int get hashCode => - asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode; + id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode; } diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index 7e3776adb2..ce3d39629f 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -53,6 +53,9 @@ class ThumbnailTile extends ConsumerWidget { ) : const BoxDecoration(); + final hasStack = + asset is RemoteAsset && (asset as RemoteAsset).stackCount > 0; + return Stack( children: [ AnimatedContainer( @@ -75,6 +78,19 @@ class ThumbnailTile extends ConsumerWidget { ), ), ), + if (hasStack) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.only( + right: 10.0, + top: asset.isVideo ? 24.0 : 6.0, + ), + child: _StackIndicator( + stackCount: (asset as RemoteAsset).stackCount, + ), + ), + ), if (asset.isVideo) Align( alignment: Alignment.topRight, @@ -182,6 +198,40 @@ class _SelectionIndicator extends StatelessWidget { } } +class _StackIndicator extends StatelessWidget { + final int stackCount; + + const _StackIndicator({required this.stackCount}); + + @override + Widget build(BuildContext context) { + return Row( + spacing: 3, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + // CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stackCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + blurRadius: 5.0, + color: Color.fromRGBO(0, 0, 0, 0.6), + ), + ], + ), + ), + const _TileOverlayIcon(Icons.burst_mode_rounded), + ], + ); + } +} + class _VideoIndicator extends StatelessWidget { final Duration duration; const _VideoIndicator(this.duration); @@ -192,8 +242,8 @@ class _VideoIndicator extends StatelessWidget { spacing: 3, mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, - // CrossAxisAlignment.end looks more centered vertically than CrossAxisAlignment.center - crossAxisAlignment: CrossAxisAlignment.end, + // CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( duration.format(), diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index a6262bd2e5..268bbc30c0 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -57,30 +57,24 @@ class DriftMemoryCard extends StatelessWidget { } if (asset.isImage) { - return Hero( - tag: 'memory-${asset.id}', - child: FullImage( - asset, - fit: fit, - size: const Size(double.infinity, double.infinity), - ), + return FullImage( + asset, + fit: fit, + size: const Size(double.infinity, double.infinity), ); } else { - return Hero( - tag: 'memory-${asset.id}', - child: SizedBox( - width: context.width, - height: context.height, - child: NativeVideoViewer( - key: ValueKey(asset.id), - asset: asset, - showControls: false, - playbackDelayFactor: 2, - image: FullImage( - asset, - size: Size(context.width, context.height), - fit: BoxFit.contain, - ), + return SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewer( + key: ValueKey(asset.id), + asset: asset, + showControls: false, + playbackDelayFactor: 2, + image: FullImage( + asset, + size: Size(context.width, context.height), + fit: BoxFit.contain, ), ), ); diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index aa21f36dd1..403d8de061 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -79,15 +79,12 @@ class DriftMemoryCard extends ConsumerWidget { Colors.black.withValues(alpha: 0.2), BlendMode.darken, ), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: SizedBox( - width: 205, - height: 200, - child: Thumbnail( - remoteId: memory.assets[0].id, - fit: BoxFit.cover, - ), + child: SizedBox( + width: 205, + height: 200, + child: Thumbnail( + remoteId: memory.assets[0].id, + fit: BoxFit.cover, ), ), ), diff --git a/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart new file mode 100644 index 0000000000..9be55cae67 --- /dev/null +++ b/mobile/lib/presentation/widgets/partner_user_avatar.widget.dart @@ -0,0 +1,32 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/services/api.service.dart'; + +class PartnerUserAvatar extends StatelessWidget { + const PartnerUserAvatar({super.key, required this.partner}); + + final PartnerUserDto partner; + + @override + Widget build(BuildContext context) { + final url = + "${Store.get(StoreKey.serverEndpoint)}/users/${partner.id}/profile-image"; + final nameFirstLetter = partner.name.isNotEmpty ? partner.name[0] : ""; + return CircleAvatar( + radius: 16, + backgroundColor: context.primaryColor.withAlpha(50), + foregroundImage: CachedNetworkImageProvider( + url, + headers: ApiService.getRequestHeaders(), + cacheKey: "user-${partner.id}-profile", + ), + // silence errors if user has no profile image, use initials as fallback + onForegroundImageError: (exception, stackTrace) {}, + child: Text(nameFirstLetter.toUpperCase()), + ); + } +} diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart new file mode 100644 index 0000000000..d32608a8b4 --- /dev/null +++ b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; + +class DriftRemoteAlbumOption extends ConsumerWidget { + const DriftRemoteAlbumOption({ + super.key, + this.onAddPhotos, + this.onAddUsers, + this.onDeleteAlbum, + this.onLeaveAlbum, + this.onCreateSharedLink, + this.onToggleAlbumOrder, + this.onEditAlbum, + }); + + final VoidCallback? onAddPhotos; + final VoidCallback? onAddUsers; + final VoidCallback? onDeleteAlbum; + final VoidCallback? onLeaveAlbum; + final VoidCallback? onCreateSharedLink; + final VoidCallback? onToggleAlbumOrder; + final VoidCallback? onEditAlbum; + + @override + Widget build(BuildContext context, WidgetRef ref) { + TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.w600, + ); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: ListView( + shrinkWrap: true, + children: [ + if (onEditAlbum != null) + ListTile( + leading: const Icon(Icons.edit), + title: Text( + 'edit_album'.t(context: context), + style: textStyle, + ), + onTap: onEditAlbum, + ), + if (onAddPhotos != null) + ListTile( + leading: const Icon(Icons.add_a_photo), + title: Text( + 'add_photos'.t(context: context), + style: textStyle, + ), + onTap: onAddPhotos, + ), + if (onAddUsers != null) + ListTile( + leading: const Icon(Icons.group_add), + title: Text( + 'album_viewer_page_share_add_users'.t(context: context), + style: textStyle, + ), + onTap: onAddUsers, + ), + if (onLeaveAlbum != null) + ListTile( + leading: const Icon(Icons.person_remove_rounded), + title: Text( + 'leave_album'.t(context: context), + style: textStyle, + ), + onTap: onLeaveAlbum, + ), + if (onToggleAlbumOrder != null) + ListTile( + leading: const Icon(Icons.swap_vert_rounded), + title: Text( + 'change_display_order'.t(context: context), + style: textStyle, + ), + onTap: onToggleAlbumOrder, + ), + if (onCreateSharedLink != null) + ListTile( + leading: const Icon(Icons.link), + title: Text( + 'create_shared_link'.t(context: context), + style: textStyle, + ), + onTap: onCreateSharedLink, + ), + if (onDeleteAlbum != null) ...[ + const Divider( + indent: 16, + endIndent: 16, + ), + ListTile( + leading: Icon( + Icons.delete, + color: + context.isDarkTheme ? Colors.red[400] : Colors.red[800], + ), + title: Text( + 'delete_album'.t(context: context), + style: textStyle.copyWith( + color: + context.isDarkTheme ? Colors.red[400] : Colors.red[800], + ), + ), + onTap: onDeleteAlbum, + ), + ], + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 30a4088ce2..6faa4da9f7 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -15,6 +15,8 @@ class TimelineArgs { final double spacing; final int columnCount; final bool showStorageIndicator; + final bool withStack; + final GroupAssetsBy? groupBy; const TimelineArgs({ required this.maxWidth, @@ -22,6 +24,8 @@ class TimelineArgs { this.spacing = kTimelineSpacing, this.columnCount = kTimelineColumnCount, this.showStorageIndicator = false, + this.withStack = false, + this.groupBy, }); @override @@ -30,7 +34,9 @@ class TimelineArgs { maxWidth == other.maxWidth && maxHeight == other.maxHeight && columnCount == other.columnCount && - showStorageIndicator == other.showStorageIndicator; + showStorageIndicator == other.showStorageIndicator && + withStack == other.withStack && + groupBy == other.groupBy; } @override @@ -39,7 +45,9 @@ class TimelineArgs { maxHeight.hashCode ^ spacing.hashCode ^ columnCount.hashCode ^ - showStorageIndicator.hashCode; + showStorageIndicator.hashCode ^ + withStack.hashCode ^ + groupBy.hashCode; } class TimelineState { @@ -97,8 +105,9 @@ final timelineSegmentProvider = StreamProvider.autoDispose>( final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1)); final tileExtent = math.max(0, availableTileWidth) / columnCount; - final groupBy = GroupAssetsBy - .values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; + final groupBy = args.groupBy ?? + GroupAssetsBy + .values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)]; final timelineService = ref.watch(timelineServiceProvider); yield* timelineService.watchBuckets().map((buckets) { diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 490f2bcff2..873908832b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -27,8 +28,14 @@ class Timeline extends StatelessWidget { this.topSliverWidget, this.topSliverWidgetHeight, this.showStorageIndicator = false, - this.appBar, + this.withStack = false, + this.appBar = const ImmichSliverAppBar( + floating: true, + pinned: false, + snap: false, + ), this.bottomSheet = const GeneralBottomSheet(), + this.groupBy, }); final Widget? topSliverWidget; @@ -36,6 +43,9 @@ class Timeline extends StatelessWidget { final bool showStorageIndicator; final Widget? appBar; final Widget? bottomSheet; + final bool withStack; + final GroupAssetsBy? groupBy; + @override Widget build(BuildContext context) { return Scaffold( @@ -50,6 +60,8 @@ class Timeline extends StatelessWidget { settingsProvider.select((s) => s.get(Setting.tilesPerRow)), ), showStorageIndicator: showStorageIndicator, + withStack: withStack, + groupBy: groupBy, ), ), ], @@ -112,13 +124,17 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return asyncSegments.widgetWhen( onData: (segments) { final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; - final statusBarHeight = context.padding.top; final double appBarExpandedHeight = widget.appBar != null && widget.appBar is MesmerizingSliverAppBar ? 200 : 0; - final totalAppBarHeight = statusBarHeight + kToolbarHeight; + final topPadding = context.padding.top + + (widget.appBar == null ? 0 : kToolbarHeight) + + 10; + const scrubberBottomPadding = 100.0; + final bottomPadding = context.padding.bottom + + (widget.appBar == null ? 0 : scrubberBottomPadding); return PrimaryScrollController( controller: _scrollController, @@ -127,8 +143,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { Scrubber( layoutSegments: segments, timelineHeight: maxHeight, - topPadding: totalAppBarHeight + 10, - bottomPadding: context.padding.bottom + scrubberBottomPadding, + topPadding: topPadding, + bottomPadding: bottomPadding, monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, child: CustomScrollView( @@ -137,13 +153,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { slivers: [ if (isSelectionMode) const SelectionSliverAppBar() - else - widget.appBar ?? - const ImmichSliverAppBar( - floating: true, - pinned: false, - snap: false, - ), + else if (widget.appBar != null) + widget.appBar!, if (widget.topSliverWidget != null) widget.topSliverWidget!, _SliverSegmentedList( segments: segments, @@ -188,21 +199,22 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { child: _MultiSelectStatusButton(), ), ), - Consumer( - builder: (_, consumerRef, child) { - final isMultiSelectEnabled = consumerRef.watch( - multiSelectProvider.select( - (s) => s.isEnabled, - ), - ); + if (widget.bottomSheet != null) + Consumer( + builder: (_, consumerRef, child) { + final isMultiSelectEnabled = consumerRef.watch( + multiSelectProvider.select( + (s) => s.isEnabled, + ), + ); - if (isMultiSelectEnabled) { - return child!; - } - return const SizedBox.shrink(); - }, - child: widget.bottomSheet, - ), + if (isMultiSelectEnabled) { + return child!; + } + return const SizedBox.shrink(); + }, + child: widget.bottomSheet, + ), ], ], ), diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 3ec7813e2f..3be46d2fbd 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -3,11 +3,15 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -16,8 +20,10 @@ import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { @@ -57,29 +63,63 @@ class AppLifeCycleNotifier extends StateNotifier { debugPrint("Using server URL: $endpoint"); } - final permission = _ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { - await _ref.read(backupProvider.notifier).resumeBackup(); - await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + if (!Store.isBetaTimelineEnabled) { + final permission = _ref.watch(galleryPermissionNotifier); + if (permission.isGranted || permission.isLimited) { + await _ref.read(backupProvider.notifier).resumeBackup(); + await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + } } await _ref.read(serverInfoProvider.notifier).getServerVersion(); } - switch (_ref.read(tabProvider)) { - case TabEnum.home: - await _ref.read(assetProvider.notifier).getAllAsset(); - break; - case TabEnum.search: - // nothing to do - break; + if (!Store.isBetaTimelineEnabled) { + switch (_ref.read(tabProvider)) { + case TabEnum.home: + await _ref.read(assetProvider.notifier).getAllAsset(); - case TabEnum.albums: - await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); - break; - case TabEnum.library: - // nothing to do - break; + case TabEnum.albums: + await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); + + case TabEnum.library: + case TabEnum.search: + break; + } + } else { + _ref.read(backupProvider.notifier).cancelBackup(); + + final backgroundManager = _ref.read(backgroundSyncProvider); + // Ensure proper cleanup before starting new background tasks + try { + await Future.wait([ + backgroundManager.syncLocal().then( + (_) { + Logger("AppLifeCycleNotifier") + .fine("Hashing assets after syncLocal"); + // Check if app is still active before hashing + if (state == AppLifeCycleEnum.resumed) { + backgroundManager.hashAssets(); + } + }, + ), + backgroundManager.syncRemote(), + ]).then((_) async { + final isEnableBackup = _ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableBackup); + + if (isEnableBackup) { + await _ref.read(driftBackupProvider.notifier).handleBackupResume(); + } + }); + } catch (e, stackTrace) { + Logger("AppLifeCycleNotifier").severe( + "Error during background sync", + e, + stackTrace, + ); + } } _ref.read(websocketProvider.notifier).connect(); @@ -92,9 +132,11 @@ class AppLifeCycleNotifier extends StateNotifier { .read(galleryPermissionNotifier.notifier) .getGalleryPermissionStatus(); - await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); + if (!Store.isBetaTimelineEnabled) { + await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - _ref.invalidate(memoryFutureProvider); + _ref.invalidate(memoryFutureProvider); + } } void handleAppInactivity() { @@ -107,23 +149,54 @@ class AppLifeCycleNotifier extends StateNotifier { _wasPaused = true; if (_ref.read(authProvider).isAuthenticated) { - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != - BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); + if (!Store.isBetaTimelineEnabled) { + // Do not cancel backup if manual upload is in progress + if (_ref.read(backupProvider.notifier).backupProgress != + BackUpProgressEnum.manualInProgress) { + _ref.read(backupProvider.notifier).cancelBackup(); + } } + _ref.read(websocketProvider.notifier).disconnect(); } - LogService.I.flush(); + try { + LogService.I.flush(); + } catch (e) { + // Ignore flush errors during pause + } } Future handleAppDetached() async { state = AppLifeCycleEnum.detached; - LogService.I.flush(); - await Isar.getInstance()?.close(); + + // Flush logs before closing database + try { + LogService.I.flush(); + } catch (e) { + // Ignore flush errors during shutdown + } + + // Close Isar database safely + try { + final isar = Isar.getInstance(); + if (isar != null && isar.isOpen) { + await isar.close(); + } + } catch (e) { + // Ignore close errors during shutdown + } + + if (Store.isBetaTimelineEnabled) { + return; + } + // no guarantee this is called at all - _ref.read(manualUploadProvider.notifier).cancelBackup(); + try { + _ref.read(manualUploadProvider.notifier).cancelBackup(); + } catch (e) { + // Ignore errors during shutdown + } } void handleAppHidden() { diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 5b77da90f3..7fbacc3afb 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -81,7 +81,9 @@ class AssetNotifier extends StateNotifier { await _albumService.refreshDeviceAlbums(); } finally { _getAllAssetInProgress = false; - state = false; + if (mounted) { + state = false; + } } } diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index ed2c485b13..3c448b112f 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; @@ -30,7 +29,7 @@ class ShareIntentUploadStateNotifier this._uploadService, this._shareIntentService, ) : super([]) { - _uploadService.onUploadStatus = _uploadStatusCallback; + _uploadService.onUploadStatus = _updateUploadStatus; _uploadService.onTaskProgress = _taskProgressCallback; } @@ -69,8 +68,8 @@ class ShareIntentUploadStateNotifier state = []; } - void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async { - if (status == TaskStatus.canceled) { + void _updateUploadStatus(TaskStatusUpdate task) async { + if (task.status == TaskStatus.canceled) { return; } @@ -83,7 +82,7 @@ class ShareIntentUploadStateNotifier TaskStatus.running => UploadStatus.running, TaskStatus.paused => UploadStatus.paused, TaskStatus.notFound => UploadStatus.notFound, - TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry + TaskStatus.waitingToRetry => UploadStatus.waitingToRetry }; state = [ @@ -95,27 +94,6 @@ class ShareIntentUploadStateNotifier ]; } - void _uploadStatusCallback(TaskStatusUpdate update) { - _updateUploadStatus(update, update.status); - - switch (update.status) { - case TaskStatus.complete: - if (update.responseStatusCode == 200) { - if (kDebugMode) { - debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE"); - } - } else { - if (kDebugMode) { - debugPrint("[COMPLETE] ${update.task.taskId}"); - } - } - break; - - default: - break; - } - } - void _taskProgressCallback(TaskProgressUpdate update) { // Ignore if the task is canceled or completed if (update.progress == downloadFailed || @@ -134,10 +112,6 @@ class ShareIntentUploadStateNotifier } Future upload(File file) { - return _uploadService.upload(file); - } - - Future cancelUpload(String id) { - return _uploadService.cancelUpload(id); + return _uploadService.buildUploadTask(file, group: kManualUploadGroup); } } diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index 83d103bb3b..dc9cc0d59f 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -1,8 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; final backgroundSyncProvider = Provider((ref) { - final manager = BackgroundSyncManager(); + final syncStatusNotifier = ref.read(syncStatusProvider.notifier); + final manager = BackgroundSyncManager( + onRemoteSyncStart: syncStatusNotifier.startRemoteSync, + onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync, + onRemoteSyncError: syncStatusNotifier.errorRemoteSync, + ); ref.onDispose(manager.cancel); return manager; }); diff --git a/mobile/lib/providers/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart new file mode 100644 index 0000000000..2915c7c216 --- /dev/null +++ b/mobile/lib/providers/backup/backup_album.provider.dart @@ -0,0 +1,64 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/services/local_album.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; + +final backupAlbumProvider = + StateNotifierProvider>( + (ref) => BackupAlbumNotifier( + ref.watch(localAlbumServiceProvider), + ), +); + +class BackupAlbumNotifier extends StateNotifier> { + BackupAlbumNotifier(this._localAlbumService) : super([]) { + getAll(); + } + + final LocalAlbumService _localAlbumService; + + Future getAll() async { + state = + await _localAlbumService.getAll(sortBy: {SortLocalAlbumsBy.assetCount}); + } + + Future selectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.selected); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.selected) + : currentAlbum, + ) + .toList(); + } + + Future deselectAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.none); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.none) + : currentAlbum, + ) + .toList(); + } + + Future excludeAlbum(LocalAlbum album) async { + album = album.copyWith(backupSelection: BackupSelection.excluded); + await _localAlbumService.update(album); + + state = state + .map( + (currentAlbum) => currentAlbum.id == album.id + ? currentAlbum.copyWith(backupSelection: BackupSelection.excluded) + : currentAlbum, + ) + .toList(); + } +} diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart new file mode 100644 index 0000000000..c51c40775e --- /dev/null +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -0,0 +1,377 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:async'; +import 'dart:convert'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/services/drift_backup.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; + +class EnqueueStatus { + final int enqueueCount; + final int totalCount; + + const EnqueueStatus({ + required this.enqueueCount, + required this.totalCount, + }); + + EnqueueStatus copyWith({ + int? enqueueCount, + int? totalCount, + }) { + return EnqueueStatus( + enqueueCount: enqueueCount ?? this.enqueueCount, + totalCount: totalCount ?? this.totalCount, + ); + } + + @override + String toString() => + 'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)'; +} + +class DriftUploadStatus { + final String taskId; + final String filename; + final double progress; + final int fileSize; + final String networkSpeedAsString; + + const DriftUploadStatus({ + required this.taskId, + required this.filename, + required this.progress, + required this.fileSize, + required this.networkSpeedAsString, + }); + + DriftUploadStatus copyWith({ + String? taskId, + String? filename, + double? progress, + int? fileSize, + String? networkSpeedAsString, + }) { + return DriftUploadStatus( + taskId: taskId ?? this.taskId, + filename: filename ?? this.filename, + progress: progress ?? this.progress, + fileSize: fileSize ?? this.fileSize, + networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString, + ); + } + + @override + String toString() { + return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString)'; + } + + @override + bool operator ==(covariant DriftUploadStatus other) { + if (identical(this, other)) return true; + + return other.taskId == taskId && + other.filename == filename && + other.progress == progress && + other.fileSize == fileSize && + other.networkSpeedAsString == networkSpeedAsString; + } + + @override + int get hashCode { + return taskId.hashCode ^ + filename.hashCode ^ + progress.hashCode ^ + fileSize.hashCode ^ + networkSpeedAsString.hashCode; + } + + Map toMap() { + return { + 'taskId': taskId, + 'filename': filename, + 'progress': progress, + 'fileSize': fileSize, + 'networkSpeedAsString': networkSpeedAsString, + }; + } + + factory DriftUploadStatus.fromMap(Map map) { + return DriftUploadStatus( + taskId: map['taskId'] as String, + filename: map['filename'] as String, + progress: map['progress'] as double, + fileSize: map['fileSize'] as int, + networkSpeedAsString: map['networkSpeedAsString'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory DriftUploadStatus.fromJson(String source) => + DriftUploadStatus.fromMap(json.decode(source) as Map); +} + +class DriftBackupState { + final int totalCount; + final int backupCount; + final int remainderCount; + + final int enqueueCount; + final int enqueueTotalCount; + + final bool isCanceling; + + final Map uploadItems; + + const DriftBackupState({ + required this.totalCount, + required this.backupCount, + required this.remainderCount, + required this.enqueueCount, + required this.enqueueTotalCount, + required this.isCanceling, + required this.uploadItems, + }); + + DriftBackupState copyWith({ + int? totalCount, + int? backupCount, + int? remainderCount, + int? enqueueCount, + int? enqueueTotalCount, + bool? isCanceling, + Map? uploadItems, + }) { + return DriftBackupState( + totalCount: totalCount ?? this.totalCount, + backupCount: backupCount ?? this.backupCount, + remainderCount: remainderCount ?? this.remainderCount, + enqueueCount: enqueueCount ?? this.enqueueCount, + enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount, + isCanceling: isCanceling ?? this.isCanceling, + uploadItems: uploadItems ?? this.uploadItems, + ); + } + + @override + String toString() { + return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)'; + } + + @override + bool operator ==(covariant DriftBackupState other) { + if (identical(this, other)) return true; + final mapEquals = const DeepCollectionEquality().equals; + + return other.totalCount == totalCount && + other.backupCount == backupCount && + other.remainderCount == remainderCount && + other.enqueueCount == enqueueCount && + other.enqueueTotalCount == enqueueTotalCount && + other.isCanceling == isCanceling && + mapEquals(other.uploadItems, uploadItems); + } + + @override + int get hashCode { + return totalCount.hashCode ^ + backupCount.hashCode ^ + remainderCount.hashCode ^ + enqueueCount.hashCode ^ + enqueueTotalCount.hashCode ^ + isCanceling.hashCode ^ + uploadItems.hashCode; + } +} + +final driftBackupProvider = + StateNotifierProvider((ref) { + return ExpBackupNotifier( + ref.watch(driftBackupServiceProvider), + ref.watch(uploadServiceProvider), + ); +}); + +class ExpBackupNotifier extends StateNotifier { + ExpBackupNotifier( + this._backupService, + this._uploadService, + ) : super( + const DriftBackupState( + totalCount: 0, + backupCount: 0, + remainderCount: 0, + enqueueCount: 0, + enqueueTotalCount: 0, + isCanceling: false, + uploadItems: {}, + ), + ) { + { + _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate); + _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate); + } + } + + final DriftBackupService _backupService; + final UploadService _uploadService; + StreamSubscription? _statusSubscription; + StreamSubscription? _progressSubscription; + + /// Remove upload item from state + void _removeUploadItem(String taskId) { + if (state.uploadItems.containsKey(taskId)) { + final updatedItems = + Map.from(state.uploadItems); + updatedItems.remove(taskId); + state = state.copyWith(uploadItems: updatedItems); + } + } + + void _handleTaskStatusUpdate(TaskStatusUpdate update) { + switch (update.status) { + case TaskStatus.complete: + if (update.task.group == kBackupGroup) { + state = state.copyWith( + backupCount: state.backupCount + 1, + remainderCount: state.remainderCount - 1, + ); + } + + // Remove the completed task from the upload items + final taskId = update.task.taskId; + if (state.uploadItems.containsKey(taskId)) { + Future.delayed(const Duration(milliseconds: 500), () { + _removeUploadItem(taskId); + }); + } + + case TaskStatus.failed: + break; + + case TaskStatus.canceled: + _removeUploadItem(update.task.taskId); + break; + + default: + break; + } + } + + void _handleTaskProgressUpdate(TaskProgressUpdate update) { + final taskId = update.task.taskId; + final filename = update.task.displayName; + final progress = update.progress; + final currentItem = state.uploadItems[taskId]; + if (currentItem != null) { + if (progress == kUploadStatusCanceled) { + _removeUploadItem(update.task.taskId); + return; + } + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: update.hasExpectedFileSize + ? currentItem.copyWith( + progress: progress, + fileSize: update.expectedFileSize, + networkSpeedAsString: update.networkSpeedAsString, + ) + : currentItem.copyWith( + progress: progress, + ), + }, + ); + + return; + } + + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + taskId: DriftUploadStatus( + taskId: taskId, + filename: filename, + progress: progress, + fileSize: update.expectedFileSize, + networkSpeedAsString: update.networkSpeedAsString, + ), + }, + ); + } + + Future getBackupStatus() async { + final [totalCount, backupCount, remainderCount] = await Future.wait([ + _backupService.getTotalCount(), + _backupService.getBackupCount(), + _backupService.getRemainderCount(), + ]); + + state = state.copyWith( + totalCount: totalCount, + backupCount: backupCount, + remainderCount: remainderCount, + ); + } + + Future backup() { + return _backupService.backup(_updateEnqueueCount); + } + + void _updateEnqueueCount(EnqueueStatus status) { + state = state.copyWith( + enqueueCount: status.enqueueCount, + enqueueTotalCount: status.totalCount, + ); + } + + Future cancel() async { + state = state.copyWith( + enqueueCount: 0, + enqueueTotalCount: 0, + isCanceling: true, + ); + + await _backupService.cancel(); + + // Check if there are any tasks left in the queue + final tasks = await FileDownloader().allTasks(group: kBackupGroup); + + debugPrint("Tasks left to cancel: ${tasks.length}"); + + if (tasks.isNotEmpty) { + await cancel(); + } else { + // Clear all upload items when cancellation is complete + state = state.copyWith( + isCanceling: false, + uploadItems: {}, + ); + } + } + + Future handleBackupResume() async { + final tasks = await FileDownloader().allTasks(group: kBackupGroup); + if (tasks.isEmpty) { + // Start a new backup queue + await backup(); + } + + debugPrint("Tasks to resume: ${tasks.length}"); + await FileDownloader().start(); + } + + @override + void dispose() { + _statusSubscription?.cancel(); + _progressSubscription?.cancel(); + super.dispose(); + } +} diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index f70bdad9dc..11cdcd54c5 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/services/gcast.service.dart'; @@ -50,10 +51,29 @@ class CastNotifier extends StateNotifier { state = state.copyWith(castState: castState); } - void loadMedia(Asset asset, bool reload) { + void loadMedia(RemoteAsset asset, bool reload) { _gCastService.loadMedia(asset, reload); } + // TODO: remove this when we migrate to new timeline + void loadMediaOld(old_asset_entity.Asset asset, bool reload) { + final remoteAsset = RemoteAsset( + id: asset.remoteId.toString(), + name: asset.name, + ownerId: asset.ownerId.toString(), + checksum: asset.checksum, + type: asset.type == old_asset_entity.AssetType.image + ? AssetType.image + : asset.type == old_asset_entity.AssetType.video + ? AssetType.video + : AssetType.other, + createdAt: asset.fileCreatedAt, + updatedAt: asset.updatedAt, + ); + + _gCastService.loadMedia(remoteAsset, reload); + } + Future connect(CastDestinationType type, dynamic device) async { switch (type) { case CastDestinationType.googleCast: diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 49605e918a..cb025ef941 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -41,34 +41,60 @@ class ActionNotifier extends Notifier { } List _getRemoteIdsForSource(ActionSource source) { - return _getIdsForSource(source).toIds().toList(); + return _getAssets(source) + .whereType() + .toIds() + .toList(growable: false); } - List _getOwnedRemoteForSource(ActionSource source) { + List _getLocalIdsForSource(ActionSource source) { + final Set assets = _getAssets(source); + final List localIds = []; + + for (final asset in assets) { + if (asset is LocalAsset) { + localIds.add(asset.id); + } else if (asset is RemoteAsset && asset.localId != null) { + localIds.add(asset.localId!); + } + } + + return localIds; + } + + List _getOwnedRemoteIdsForSource(ActionSource source) { final ownerId = ref.read(currentUserProvider)?.id; - return _getIdsForSource(source) + return _getAssets(source) + .whereType() .ownedAssets(ownerId) .toIds() - .toList(); + .toList(growable: false); + } + + List _getOwnedRemoteAssetsForSource(ActionSource source) { + final ownerId = ref.read(currentUserProvider)?.id; + return _getIdsForSource(source).ownedAssets(ownerId).toList(); } Iterable _getIdsForSource(ActionSource source) { - final Set assets = switch (source) { - ActionSource.timeline => - ref.read(multiSelectProvider.select((s) => s.selectedAssets)), - ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { - BaseAsset asset => {asset}, - null => {}, - }, - }; - + final Set assets = _getAssets(source); return switch (T) { const (RemoteAsset) => assets.whereType(), const (LocalAsset) => assets.whereType(), - _ => [], + _ => const [], } as Iterable; } + Set _getAssets(ActionSource source) { + return switch (source) { + ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, + ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { + BaseAsset asset => {asset}, + null => const {}, + }, + }; + } + Future shareLink( ActionSource source, BuildContext context, @@ -88,7 +114,7 @@ class ActionNotifier extends Notifier { } Future favorite(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.favorite(ids); return ActionResult(count: ids.length, success: true); @@ -103,7 +129,7 @@ class ActionNotifier extends Notifier { } Future unFavorite(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.unFavorite(ids); return ActionResult(count: ids.length, success: true); @@ -118,7 +144,7 @@ class ActionNotifier extends Notifier { } Future archive(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.archive(ids); return ActionResult(count: ids.length, success: true); @@ -133,7 +159,7 @@ class ActionNotifier extends Notifier { } Future unArchive(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.unArchive(ids); return ActionResult(count: ids.length, success: true); @@ -148,9 +174,10 @@ class ActionNotifier extends Notifier { } Future moveToLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); + final localIds = _getLocalIdsForSource(source); try { - await _service.moveToLockFolder(ids); + await _service.moveToLockFolder(ids, localIds); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to move assets to lock folder', error, stack); @@ -163,7 +190,7 @@ class ActionNotifier extends Notifier { } Future removeFromLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.removeFromLockFolder(ids); return ActionResult(count: ids.length, success: true); @@ -178,7 +205,7 @@ class ActionNotifier extends Notifier { } Future trash(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.trash(ids); return ActionResult(count: ids.length, success: true); @@ -193,7 +220,7 @@ class ActionNotifier extends Notifier { } Future delete(ActionSource source) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { await _service.delete(ids); return ActionResult(count: ids.length, success: true); @@ -207,11 +234,26 @@ class ActionNotifier extends Notifier { } } + Future deleteLocal(ActionSource source) async { + final ids = _getLocalIdsForSource(source); + try { + await _service.deleteLocal(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to delete assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + Future editLocation( ActionSource source, BuildContext context, ) async { - final ids = _getOwnedRemoteForSource(source); + final ids = _getOwnedRemoteIdsForSource(source); try { final isEdited = await _service.editLocation(ids, context); if (!isEdited) { @@ -246,13 +288,76 @@ class ActionNotifier extends Notifier { ); } } + + Future stack(String userId, ActionSource source) async { + final ids = _getOwnedRemoteIdsForSource(source); + try { + await _service.stack(userId, ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to stack assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future unStack(ActionSource source) async { + final assets = _getOwnedRemoteAssetsForSource(source); + try { + await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); + return ActionResult(count: assets.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unstack assets', error, stack); + return ActionResult( + count: assets.length, + success: false, + ); + } + } + + Future shareAssets(ActionSource source) async { + final ids = _getAssets(source).toList(growable: false); + + try { + final count = await _service.shareAssets(ids); + return ActionResult(count: count, success: true); + } catch (error, stack) { + _logger.severe('Failed to share assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future downloadAll(ActionSource source) async { + final assets = + _getAssets(source).whereType().toList(growable: false); + + try { + final didEnqueue = await _service.downloadAll(assets); + final enqueueCount = didEnqueue.where((e) => e).length; + return ActionResult(count: enqueueCount, success: true); + } catch (error, stack) { + _logger.severe('Failed to download assets', error, stack); + return ActionResult( + count: assets.length, + success: false, + error: error.toString(), + ); + } + } } extension on Iterable { Iterable toIds() => map((e) => e.id); Iterable ownedAssets(String? ownerId) { - if (ownerId == null) return []; + if (ownerId == null) return const []; return whereType().where((a) => a.ownerId == ownerId); } } diff --git a/mobile/lib/providers/infrastructure/current_album.provider.dart b/mobile/lib/providers/infrastructure/current_album.provider.dart new file mode 100644 index 0000000000..ece188ee15 --- /dev/null +++ b/mobile/lib/providers/infrastructure/current_album.provider.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; + +final currentRemoteAlbumProvider = + AutoDisposeNotifierProvider( + CurrentAlbumNotifier.new, +); + +class CurrentAlbumNotifier extends AutoDisposeNotifier { + KeepAliveLink? _keepAliveLink; + StreamSubscription? _assetSubscription; + + @override + RemoteAlbum? build() => null; + + void setAlbum(RemoteAlbum album) { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + state = album; + + _assetSubscription = ref + .watch(remoteAlbumServiceProvider) + .watchAlbum(album.id) + .listen((updatedAlbum) { + if (updatedAlbum != null) { + state = updatedAlbum; + } + }); + _keepAliveLink = ref.keepAlive(); + } + + void dispose() { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); + } +} diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index 4eefbc556c..cdf934e508 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -13,5 +13,6 @@ Isar isar(Ref ref) => throw UnimplementedError('isar'); final driftProvider = Provider((ref) { final drift = Drift(); ref.onDispose(() => unawaited(drift.close())); + ref.keepAlive(); return drift; }); diff --git a/mobile/lib/providers/infrastructure/partner.provider.dart b/mobile/lib/providers/infrastructure/partner.provider.dart new file mode 100644 index 0000000000..e1c7ebf960 --- /dev/null +++ b/mobile/lib/providers/infrastructure/partner.provider.dart @@ -0,0 +1,92 @@ +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/partner.service.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class PartnerNotifier extends Notifier> { + late DriftPartnerService _driftPartnerService; + + @override + List build() { + _driftPartnerService = ref.read(driftPartnerServiceProvider); + return []; + } + + Future _loadPartners() async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + state = await _driftPartnerService.getSharedWith(currentUser.id); + } + + Future> getPartners(String userId) async { + final partners = await _driftPartnerService.getSharedWith(userId); + state = partners; + return partners; + } + + Future toggleShowInTimeline(String partnerId, String userId) async { + await _driftPartnerService.toggleShowInTimeline(partnerId, userId); + await _loadPartners(); + } + + Future addPartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.addPartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } + + Future removePartner(PartnerUserDto partner) async { + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + return; + } + + await _driftPartnerService.removePartner(partner.id, currentUser.id); + await _loadPartners(); + ref.invalidate(driftAvailablePartnerProvider); + ref.invalidate(driftSharedByPartnerProvider); + } +} + +final driftAvailablePartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref + .watch(driftPartnerServiceProvider) + .getAvailablePartners(currentUser.id); +}); + +final driftSharedByPartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedBy(currentUser.id); +}); + +final driftSharedWithPartnerProvider = + FutureProvider.autoDispose>((ref) { + final currentUser = ref.watch(currentUserProvider); + if (currentUser == null) { + return []; + } + + return ref.watch(driftPartnerServiceProvider).getSharedWith(currentUser.id); +}); diff --git a/mobile/lib/providers/infrastructure/person.provider.dart b/mobile/lib/providers/infrastructure/person.provider.dart new file mode 100644 index 0000000000..a733104b33 --- /dev/null +++ b/mobile/lib/providers/infrastructure/person.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/person.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftPersonProvider = Provider( + (ref) => DriftPersonRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 84db53ab9f..2ce10d7cbd 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -1,8 +1,12 @@ import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/utils/remote_album.utils.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'album.provider.dart'; @@ -10,33 +14,25 @@ import 'album.provider.dart'; class RemoteAlbumState { final List albums; final List filteredAlbums; - final bool isLoading; - final String? error; const RemoteAlbumState({ required this.albums, List? filteredAlbums, - this.isLoading = false, - this.error, }) : filteredAlbums = filteredAlbums ?? albums; RemoteAlbumState copyWith({ List? albums, List? filteredAlbums, - bool? isLoading, - String? error, }) { return RemoteAlbumState( albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums, - isLoading: isLoading ?? this.isLoading, - error: error ?? this.error, ); } @override String toString() => - 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length}, isLoading: $isLoading, error: $error)'; + 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})'; @override bool operator ==(covariant RemoteAlbumState other) { @@ -44,47 +40,38 @@ class RemoteAlbumState { final listEquals = const DeepCollectionEquality().equals; return listEquals(other.albums, albums) && - listEquals(other.filteredAlbums, filteredAlbums) && - other.isLoading == isLoading && - other.error == error; + listEquals(other.filteredAlbums, filteredAlbums); } @override - int get hashCode => - albums.hashCode ^ - filteredAlbums.hashCode ^ - isLoading.hashCode ^ - error.hashCode; + int get hashCode => albums.hashCode ^ filteredAlbums.hashCode; } class RemoteAlbumNotifier extends Notifier { - late final RemoteAlbumService _remoteAlbumService; - + late RemoteAlbumService _remoteAlbumService; + final _logger = Logger('RemoteAlbumNotifier'); @override RemoteAlbumState build() { _remoteAlbumService = ref.read(remoteAlbumServiceProvider); return const RemoteAlbumState(albums: [], filteredAlbums: []); } - Future> getAll() async { - state = state.copyWith(isLoading: true, error: null); - + Future> _getAll() async { try { final albums = await _remoteAlbumService.getAll(); state = state.copyWith( albums: albums, filteredAlbums: albums, - isLoading: false, ); return albums; - } catch (e) { - state = state.copyWith(isLoading: false, error: e.toString()); + } catch (error, stack) { + _logger.severe('Failed to fetch albums', error, stack); rethrow; } } Future refresh() async { - await getAll(); + await _getAll(); } void searchAlbums( @@ -124,8 +111,6 @@ class RemoteAlbumNotifier extends Notifier { String? description, List assetIds = const [], }) async { - state = state.copyWith(isLoading: true, error: null); - try { final album = await _remoteAlbumService.createAlbum( title: title, @@ -138,11 +123,109 @@ class RemoteAlbumNotifier extends Notifier { filteredAlbums: [...state.filteredAlbums, album], ); - state = state.copyWith(isLoading: false); return album; - } catch (e) { - state = state.copyWith(isLoading: false, error: e.toString()); + } catch (error, stack) { + _logger.severe('Failed to create album', error, stack); rethrow; } } + + Future updateAlbum( + String albumId, { + String? name, + String? description, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + }) async { + try { + final updatedAlbum = await _remoteAlbumService.updateAlbum( + albumId, + name: name, + description: description, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ); + + final updatedAlbums = state.albums.map((album) { + return album.id == albumId ? updatedAlbum : album; + }).toList(); + + final updatedFilteredAlbums = state.filteredAlbums.map((album) { + return album.id == albumId ? updatedAlbum : album; + }).toList(); + + state = state.copyWith( + albums: updatedAlbums, + filteredAlbums: updatedFilteredAlbums, + ); + + return updatedAlbum; + } catch (error, stack) { + _logger.severe('Failed to update album', error, stack); + rethrow; + } + } + + Future toggleAlbumOrder(String albumId) async { + final currentAlbum = + state.albums.firstWhere((album) => album.id == albumId); + + final newOrder = currentAlbum.order == AlbumAssetOrder.asc + ? AlbumAssetOrder.desc + : AlbumAssetOrder.asc; + + return updateAlbum(albumId, order: newOrder); + } + + Future deleteAlbum(String albumId) async { + await _remoteAlbumService.deleteAlbum(albumId); + + final updatedAlbums = + state.albums.where((album) => album.id != albumId).toList(); + final updatedFilteredAlbums = + state.filteredAlbums.where((album) => album.id != albumId).toList(); + + state = state.copyWith( + albums: updatedAlbums, + filteredAlbums: updatedFilteredAlbums, + ); + } + + Future> getAssets(String albumId) { + return _remoteAlbumService.getAssets(albumId); + } + + Future addAssets(String albumId, List assetIds) { + return _remoteAlbumService.addAssets( + albumId: albumId, + assetIds: assetIds, + ); + } + + Future addUsers(String albumId, List userIds) { + return _remoteAlbumService.addUsers( + albumId: albumId, + userIds: userIds, + ); + } } + +final remoteAlbumDateRangeProvider = + FutureProvider.family<(DateTime, DateTime), String>( + (ref, albumId) async { + final service = ref.watch(remoteAlbumServiceProvider); + return service.getDateRange(albumId); + }, +); + +final remoteAlbumSharedUsersProvider = + FutureProvider.autoDispose.family, String>( + (ref, albumId) async { + final link = ref.keepAlive(); + ref.onDispose(() => link.close()); + final service = ref.watch(remoteAlbumServiceProvider); + return service.getSharedUsers(albumId); + }, +); diff --git a/mobile/lib/providers/infrastructure/search.provider.dart b/mobile/lib/providers/infrastructure/search.provider.dart new file mode 100644 index 0000000000..cdcd3ee43b --- /dev/null +++ b/mobile/lib/providers/infrastructure/search.provider.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/search.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; + +final searchApiRepositoryProvider = Provider( + (ref) => SearchApiRepository(ref.watch(apiServiceProvider).searchApi), +); + +final searchServiceProvider = Provider( + (ref) => SearchService(ref.watch(searchApiRepositoryProvider)), +); diff --git a/mobile/lib/providers/stack.provider.dart b/mobile/lib/providers/infrastructure/stack.provider.dart similarity index 100% rename from mobile/lib/providers/stack.provider.dart rename to mobile/lib/providers/infrastructure/stack.provider.dart diff --git a/mobile/lib/providers/infrastructure/user.provider.dart b/mobile/lib/providers/infrastructure/user.provider.dart index ca65f8be14..d328f97600 100644 --- a/mobile/lib/providers/infrastructure/user.provider.dart +++ b/mobile/lib/providers/infrastructure/user.provider.dart @@ -1,10 +1,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.provider.g.dart'; @@ -23,3 +28,20 @@ UserService userService(Ref ref) => UserService( userApiRepository: ref.watch(userApiRepositoryProvider), storeService: ref.watch(storeServiceProvider), ); + +/// Drifts +final driftPartnerRepositoryProvider = Provider( + (ref) => DriftPartnerRepository(ref.watch(driftProvider)), +); + +final driftPartnerServiceProvider = Provider( + (ref) => DriftPartnerService( + ref.watch(driftPartnerRepositoryProvider), + ref.watch(partnerApiRepositoryProvider), + ), +); + +final partnerUsersProvider = + NotifierProvider>( + PartnerNotifier.new, +); diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index d03d533aaf..f6ac9d1125 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -9,7 +9,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @riverpod -Future> getAllPeople( +Future> getAllPeople( Ref ref, ) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index 391edd362c..4625891abb 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -6,11 +6,12 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getAllPeopleHash() => r'226947af3b09ce62224916543958dd1d5e2ba651'; +String _$getAllPeopleHash() => r'2c5e6a207683f15ab209650615fdf9cb7f76c736'; /// See also [getAllPeople]. @ProviderFor(getAllPeople) -final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( +final getAllPeopleProvider = + AutoDisposeFutureProvider>.internal( getAllPeople, name: r'getAllPeopleProvider', debugGetCreateSourceHash: @@ -21,7 +22,7 @@ final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'c1d35ee0e024bd6915e21bc724be4b458a14bc24'; /// Copied from Dart SDK diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart new file mode 100644 index 0000000000..18d851aa19 --- /dev/null +++ b/mobile/lib/providers/sync_status.provider.dart @@ -0,0 +1,68 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +enum SyncStatus { + idle, + syncing, + success, + error, +} + +class SyncStatusState { + final SyncStatus remoteSyncStatus; + final String? errorMessage; + + const SyncStatusState({ + this.remoteSyncStatus = SyncStatus.idle, + this.errorMessage, + }); + + SyncStatusState copyWith({ + SyncStatus? remoteSyncStatus, + String? errorMessage, + }) { + return SyncStatusState( + remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SyncStatusState && + other.remoteSyncStatus == remoteSyncStatus && + other.errorMessage == errorMessage; + } + + @override + int get hashCode => Object.hash(remoteSyncStatus, errorMessage); +} + +class SyncStatusNotifier extends Notifier { + @override + SyncStatusState build() { + return const SyncStatusState( + errorMessage: null, + remoteSyncStatus: SyncStatus.idle, + ); + } + + void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) { + state = state.copyWith( + remoteSyncStatus: status, + errorMessage: status == SyncStatus.error ? errorMessage : null, + ); + } + + void startRemoteSync() => setRemoteSyncStatus(SyncStatus.syncing); + void completeRemoteSync() => setRemoteSyncStatus(SyncStatus.success); + void errorRemoteSync(String error) => + setRemoteSyncStatus(SyncStatus.error, error); +} + +final syncStatusProvider = + NotifierProvider( + SyncStatusNotifier.new, +); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index d9db831776..2718738286 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; +// import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -176,16 +177,20 @@ class WebsocketNotifier extends StateNotifier { ); }); - socket.on('on_upload_success', _handleOnUploadSuccess); + if (!Store.isBetaTimelineEnabled) { + socket.on('on_upload_success', _handleOnUploadSuccess); + socket.on('on_asset_delete', _handleOnAssetDelete); + socket.on('on_asset_trash', _handleOnAssetTrash); + socket.on('on_asset_restore', _handleServerUpdates); + socket.on('on_asset_update', _handleServerUpdates); + socket.on('on_asset_stack_update', _handleServerUpdates); + socket.on('on_asset_hidden', _handleOnAssetHidden); + } else { + socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + } + socket.on('on_config_update', _handleOnConfigUpdate); - socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); - socket.on('on_asset_restore', _handleServerUpdates); - socket.on('on_asset_update', _handleServerUpdates); - socket.on('on_asset_stack_update', _handleServerUpdates); - socket.on('on_asset_hidden', _handleOnAssetHidden); socket.on('on_new_release', _handleReleaseUpdates); - socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); } catch (e) { debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); } @@ -209,10 +214,37 @@ class WebsocketNotifier extends StateNotifier { } void stopListenToEvent(String eventName) { - debugPrint("Stop listening to event $eventName"); state.socket?.off(eventName); } + void stopListenToOldEvents() { + state.socket?.off('on_upload_success'); + state.socket?.off('on_asset_delete'); + state.socket?.off('on_asset_trash'); + state.socket?.off('on_asset_restore'); + state.socket?.off('on_asset_update'); + state.socket?.off('on_asset_stack_update'); + state.socket?.off('on_asset_hidden'); + } + + void startListeningToOldEvents() { + state.socket?.on('on_upload_success', _handleOnUploadSuccess); + state.socket?.on('on_asset_delete', _handleOnAssetDelete); + state.socket?.on('on_asset_trash', _handleOnAssetTrash); + state.socket?.on('on_asset_restore', _handleServerUpdates); + state.socket?.on('on_asset_update', _handleServerUpdates); + state.socket?.on('on_asset_stack_update', _handleServerUpdates); + state.socket?.on('on_asset_hidden', _handleOnAssetHidden); + } + + void stopListeningToBetaEvents() { + state.socket?.off('AssetUploadReadyV1'); + } + + void startListeningToBetaEvents() { + state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + } + void listenUploadEvent() { debugPrint("Start listening to event on_upload_success"); state.socket?.on('on_upload_success', _handleOnUploadSuccess); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 0dff309172..4c854973b1 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,5 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; @@ -10,14 +12,16 @@ final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, ref.watch(apiServiceProvider).searchApi, + ref.watch(apiServiceProvider).stacksApi, ), ); class AssetApiRepository extends ApiRepository { final AssetsApi _api; final SearchApi _searchApi; + final StacksApi _stacksApi; - AssetApiRepository(this._api, this._searchApi); + AssetApiRepository(this._api, this._searchApi, this._stacksApi); Future update(String id, {String? description}) async { final response = await checkNull( @@ -83,6 +87,21 @@ class AssetApiRepository extends ApiRepository { ); } + Future stack(List ids) async { + final responseDto = + await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids))); + + return responseDto.toStack(); + } + + Future unStack(List ids) async { + return _stacksApi.deleteStacks(BulkIdsDto(ids: ids)); + } + + Future downloadAsset(String id) { + return _api.downloadAssetWithHttpInfo(id); + } + _mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) { AssetVisibilityEnum.timeline => AssetVisibility.timeline, AssetVisibilityEnum.hidden => AssetVisibility.hidden, @@ -97,3 +116,13 @@ class AssetApiRepository extends ApiRepository { return response.originalMimeType; } } + +extension on StackResponseDto { + StackResponse toStack() { + return StackResponse( + id: id, + primaryAssetId: primaryAssetId, + assetIds: assets.map((asset) => asset.id).toList(), + ); + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index bb1e6f414f..8708ce9cfd 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -1,27 +1,40 @@ +import 'dart:io'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/utils/hash.dart'; -import 'package:photo_manager/photo_manager.dart' hide AssetType; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; +import 'package:share_plus/share_plus.dart'; -final assetMediaRepositoryProvider = - Provider((ref) => const AssetMediaRepository()); +final assetMediaRepositoryProvider = Provider( + (ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)), +); class AssetMediaRepository { - const AssetMediaRepository(); + final AssetApiRepository _assetApiRepository; + static final Logger _log = Logger("AssetMediaRepository"); + + const AssetMediaRepository(this._assetApiRepository); + Future> deleteAll(List ids) => PhotoManager.editor.deleteWithIds(ids); - Future get(String id) async { + Future get(String id) async { final entity = await AssetEntity.fromId(id); return toAsset(entity); } - static Asset? toAsset(AssetEntity? local) { + static asset_entity.Asset? toAsset(AssetEntity? local) { if (local == null) return null; - final Asset asset = Asset( + final asset_entity.Asset asset = asset_entity.Asset( checksum: "", localId: local.id, ownerId: fastHash(Store.get(StoreKey.currentUser).id), @@ -29,7 +42,7 @@ class AssetMediaRepository { fileModifiedAt: local.modifiedDateTime, updatedAt: local.modifiedDateTime, durationInSeconds: local.duration, - type: AssetType.values[local.typeInt], + type: asset_entity.AssetType.values[local.typeInt], fileName: local.title!, width: local.width, height: local.height, @@ -57,4 +70,57 @@ class AssetMediaRepository { // otherwise using the `entity.title` would return a random GUID return await entity.titleAsync; } + + // TODO: make this more efficient + Future shareAssets(List assets) async { + final downloadedXFiles = []; + + for (var asset in assets) { + final localId = (asset is LocalAsset) + ? asset.id + : asset is RemoteAsset + ? asset.localId + : null; + if (localId != null) { + File? f = + await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0) + .originFile; + downloadedXFiles.add(XFile(f!.path)); + } else if (asset is RemoteAsset) { + final tempDir = await getTemporaryDirectory(); + final name = asset.name; + final tempFile = await File('${tempDir.path}/$name').create(); + final res = await _assetApiRepository.downloadAsset(asset.id); + + if (res.statusCode != 200) { + _log.severe("Download for $name failed", res.toLoggerString()); + continue; + } + + await tempFile.writeAsBytes(res.bodyBytes); + downloadedXFiles.add(XFile(tempFile.path)); + } else { + _log.warning("Asset type not supported for sharing: $asset"); + continue; + } + } + + if (downloadedXFiles.isEmpty) { + _log.warning("No asset can be retrieved for share"); + return 0; + } + + final result = await Share.shareXFiles(downloadedXFiles); + + for (var file in downloadedXFiles) { + try { + await File(file.path).delete(); + } catch (e) { + _log.warning("Failed to delete temporary file: ${file.path}", e); + } + } + return result.status == ShareResultStatus.success + ? downloadedXFiles.length + : 0; + } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 4ee4d8c131..5cf357d5a4 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -24,7 +24,26 @@ class AuthRepository extends DatabaseRepository { const AuthRepository(super.db, this._drift); - Future clearLocalData() { + Future clearLocalData() async { + // Drift deletions - child entities first (those with foreign keys) + await Future.wait([ + _drift.memoryAssetEntity.deleteAll(), + _drift.remoteAlbumAssetEntity.deleteAll(), + _drift.remoteAlbumUserEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), + _drift.userMetadataEntity.deleteAll(), + _drift.partnerEntity.deleteAll(), + _drift.stackEntity.deleteAll(), + _drift.personEntity.deleteAll(), + ]); + // Drift deletions - parent entities + await Future.wait([ + _drift.memoryEntity.deleteAll(), + _drift.remoteAlbumEntity.deleteAll(), + _drift.remoteAssetEntity.deleteAll(), + _drift.userEntity.deleteAll(), + ]); + return db.writeTxn(() { return Future.wait([ db.assets.clear(), @@ -32,17 +51,6 @@ class AuthRepository extends DatabaseRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), - _drift.remoteAssetEntity.deleteAll(), - _drift.remoteExifEntity.deleteAll(), - _drift.userEntity.deleteAll(), - _drift.userMetadataEntity.deleteAll(), - _drift.partnerEntity.deleteAll(), - _drift.remoteAlbumEntity.deleteAll(), - _drift.remoteAlbumAssetEntity.deleteAll(), - _drift.remoteAlbumUserEntity.deleteAll(), - _drift.memoryEntity.deleteAll(), - _drift.memoryAssetEntity.deleteAll(), - _drift.stackEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart index 72f7e065ca..f1dae3c251 100644 --- a/mobile/lib/repositories/download.repository.dart +++ b/mobile/lib/repositories/download.repository.dart @@ -1,10 +1,28 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; +import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/download.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; final downloadRepositoryProvider = Provider((ref) => DownloadRepository()); class DownloadRepository { + static final _downloader = FileDownloader(); + static final _dummyTask = DownloadTask( + taskId: 'dummy', + url: '', + filename: 'dummy', + group: '', + updates: Updates.statusAndProgress, + ); + static final _dummyMetadata = {'part': LivePhotosPart.image, 'id': ''}; + void Function(TaskStatusUpdate)? onImageDownloadStatus; void Function(TaskStatusUpdate)? onVideoDownloadStatus; @@ -14,19 +32,19 @@ class DownloadRepository { void Function(TaskProgressUpdate)? onTaskProgress; DownloadRepository() { - FileDownloader().registerCallbacks( + _downloader.registerCallbacks( group: downloadGroupImage, taskStatusCallback: (update) => onImageDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); - FileDownloader().registerCallbacks( + _downloader.registerCallbacks( group: downloadGroupVideo, taskStatusCallback: (update) => onVideoDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); - FileDownloader().registerCallbacks( + _downloader.registerCallbacks( group: downloadGroupLivePhoto, taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), @@ -34,25 +52,87 @@ class DownloadRepository { } Future> downloadAll(List tasks) { - return FileDownloader().enqueueAll(tasks); + return _downloader.enqueueAll(tasks); } Future deleteAllTrackingRecords() { - return FileDownloader().database.deleteAllRecords(); + return _downloader.database.deleteAllRecords(); } Future cancel(String id) { - return FileDownloader().cancelTaskWithId(id); + return _downloader.cancelTaskWithId(id); } Future> getLiveVideoTasks() { - return FileDownloader().database.allRecordsWithStatus( - TaskStatus.complete, - group: downloadGroupLivePhoto, - ); + return _downloader.database.allRecordsWithStatus( + TaskStatus.complete, + group: downloadGroupLivePhoto, + ); } Future deleteRecordsWithIds(List ids) { - return FileDownloader().database.deleteRecordsWithIds(ids); + return _downloader.database.deleteRecordsWithIds(ids); + } + + Future> downloadAllAssets(List assets) async { + if (assets.isEmpty) { + return Future.value(const []); + } + + final length = Platform.isAndroid ? assets.length : assets.length * 2; + final tasks = List.filled(length, _dummyTask); + int taskIndex = 0; + final headers = ApiService.getRequestHeaders(); + for (final asset in assets) { + if (!asset.isRemoteOnly) { + continue; + } + + final id = asset.id; + final livePhotoVideoId = asset.livePhotoVideoId; + final isVideo = asset.isVideo; + final url = getOriginalUrlForRemoteId(id); + + if (Platform.isAndroid || livePhotoVideoId == null || isVideo) { + tasks[taskIndex++] = DownloadTask( + taskId: id, + url: url, + headers: headers, + filename: asset.name, + updates: Updates.statusAndProgress, + group: isVideo ? downloadGroupVideo : downloadGroupImage, + ); + continue; + } + + _dummyMetadata['part'] = LivePhotosPart.image; + _dummyMetadata['id'] = id; + tasks[taskIndex++] = DownloadTask( + taskId: id, + url: url, + headers: headers, + filename: asset.name, + updates: Updates.statusAndProgress, + group: downloadGroupLivePhoto, + metaData: json.encode(_dummyMetadata), + ); + + _dummyMetadata['part'] = LivePhotosPart.video; + tasks[taskIndex++] = DownloadTask( + taskId: livePhotoVideoId, + url: url, + headers: headers, + filename: asset.name + .toUpperCase() + .replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), + updates: Updates.statusAndProgress, + group: downloadGroupLivePhoto, + metaData: json.encode(_dummyMetadata), + ); + } + if (taskIndex == 0) { + return Future.value(const []); + } + return _downloader.enqueueAll(tasks.slice(0, taskIndex)); } } diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index 7ef24f1e7c..26b55fbef6 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -52,6 +52,77 @@ class DriftAlbumApiRepository extends ApiRepository { } return (removed: removed, failed: failed); } + + Future<({List added, List failed})> addAssets( + String albumId, + Iterable assetIds, + ) async { + final response = await checkNull( + _api.addAssetsToAlbum( + albumId, + BulkIdsDto(ids: assetIds.toList()), + ), + ); + final List added = [], failed = []; + for (final dto in response) { + if (dto.success) { + added.add(dto.id); + } else { + failed.add(dto.id); + } + } + + return (added: added, failed: failed); + } + + Future updateAlbum( + String albumId, { + String? name, + String? description, + String? thumbnailAssetId, + bool? isActivityEnabled, + AlbumAssetOrder? order, + }) async { + AssetOrder? apiOrder; + if (order != null) { + apiOrder = + order == AlbumAssetOrder.asc ? AssetOrder.asc : AssetOrder.desc; + } + + final responseDto = await checkNull( + _api.updateAlbumInfo( + albumId, + UpdateAlbumDto( + albumName: name, + description: description, + albumThumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: apiOrder, + ), + ), + ); + + return responseDto.toRemoteAlbum(); + } + + Future deleteAlbum(String albumId) { + return _api.deleteAlbum(albumId); + } + + Future addUsers( + String albumId, + Iterable userIds, + ) async { + final albumUsers = + userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); + final response = await checkNull( + _api.addUsersToAlbum( + albumId, + AddUsersDto(albumUsers: albumUsers), + ), + ); + return response.toRemoteAlbum(); + } } extension on AlbumResponseDto { diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart index a2a6e2489b..26f11dd51d 100644 --- a/mobile/lib/repositories/person_api.repository.dart +++ b/mobile/lib/repositories/person_api.repository.dart @@ -13,19 +13,19 @@ class PersonApiRepository extends ApiRepository { PersonApiRepository(this._api); - Future> getAll() async { + Future> getAll() async { final dto = await checkNull(_api.getAllPeople()); return dto.people.map(_toPerson).toList(); } - Future update(String id, {String? name}) async { + Future update(String id, {String? name}) async { final dto = await checkNull( _api.updatePerson(id, PersonUpdateDto(name: name)), ); return _toPerson(dto); } - static Person _toPerson(PersonResponseDto dto) => Person( + static PersonDto _toPerson(PersonResponseDto dto) => PersonDto( birthDate: dto.birthDate, id: dto.id, isHidden: dto.isHidden, diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 4f840fa3c6..b98eece656 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -1,6 +1,6 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/utils/upload.dart'; +import 'package:immich_mobile/constants/constants.dart'; final uploadRepositoryProvider = Provider((ref) => UploadRepository()); @@ -11,25 +11,30 @@ class UploadRepository { UploadRepository() { FileDownloader().registerCallbacks( - group: uploadGroup, + group: kBackupGroup, + taskStatusCallback: (update) => onUploadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + FileDownloader().registerCallbacks( + group: kBackupLivePhotoGroup, taskStatusCallback: (update) => onUploadStatus?.call(update), taskProgressCallback: (update) => onTaskProgress?.call(update), ); } - Future upload(UploadTask task) { - return FileDownloader().enqueue(task); + void enqueueAll(List tasks) { + FileDownloader().enqueueAll(tasks); } - Future deleteAllTrackingRecords() { - return FileDownloader().database.deleteAllRecords(); + Future deleteAllTrackingRecords(String group) { + return FileDownloader().database.deleteAllRecords(group: group); } - Future cancel(String id) { - return FileDownloader().cancelTaskWithId(id); + Future cancelAll(String group) { + return FileDownloader().cancelAll(group: group); } - Future deleteRecordsWithIds(List ids) { - return FileDownloader().database.deleteRecordsWithIds(ids); + Future reset(String group) { + return FileDownloader().reset(group: group); } } diff --git a/mobile/lib/repositories/widget.repository.dart b/mobile/lib/repositories/widget.repository.dart index be314a281e..09532f4b78 100644 --- a/mobile/lib/repositories/widget.repository.dart +++ b/mobile/lib/repositories/widget.repository.dart @@ -10,8 +10,11 @@ class WidgetRepository { await HomeWidget.saveWidgetData(key, value); } - Future refresh(String name) async { - await HomeWidget.updateWidget(name: name, iOSName: name); + Future refresh(String iosName, String androidName) async { + await HomeWidget.updateWidget( + iOSName: iosName, + qualifiedAndroidName: androidName, + ); } Future setAppGroupId(String appGroupId) async { diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index 047e897c8e..98560018ee 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -25,7 +25,7 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { _handleLockedViewState(route, previousRoute); - + _handleDriftLockedFolderState(route, previousRoute); Future( () => ref.read(currentRouteNameProvider.notifier).state = route.settings.name, @@ -54,4 +54,27 @@ class AppNavigationObserver extends AutoRouterObserver { ); } } + + _handleDriftLockedFolderState(Route route, Route? previousRoute) { + final isInLockedView = ref.read(inLockedViewProvider); + final isFromLockedViewToDetailView = + route.settings.name == AssetViewerRoute.name && + previousRoute?.settings.name == DriftLockedFolderRoute.name; + + final isFromDetailViewToInfoPanelView = route.settings.name == null && + previousRoute?.settings.name == AssetViewerRoute.name && + isInLockedView; + + if (route.settings.name == DriftLockedFolderRoute.name || + isFromLockedViewToDetailView || + isFromDetailViewToInfoPanelView) { + Future( + () => ref.read(inLockedViewProvider.notifier).state = true, + ); + } else { + Future( + () => ref.read(inLockedViewProvider.notifier).state = false, + ); + } + } } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7cd628606b..ba31ccef2b 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -22,13 +22,17 @@ import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart' import 'package:immich_mobile/pages/album/album_viewer.page.dart'; import 'package:immich_mobile/pages/albums/albums.page.dart'; import 'package:immich_mobile/pages/backup/album_preview.page.dart'; +import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; +import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; +import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; +import 'package:immich_mobile/pages/common/change_experience.page.dart'; import 'package:immich_mobile/pages/common/create_album.page.dart'; import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; @@ -47,6 +51,7 @@ import 'package:immich_mobile/pages/library/library.page.dart'; import 'package:immich_mobile/pages/library/local_albums.page.dart'; import 'package:immich_mobile/pages/library/locked/locked.page.dart'; import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; +import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; @@ -69,26 +74,28 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; import 'package:immich_mobile/presentation/pages/drift_place.page.dart'; import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart'; import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; -import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; -import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; +import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -102,7 +109,6 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; - import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -188,7 +194,7 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: SearchRoute.page, + page: DriftSearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false, ), @@ -383,6 +389,14 @@ class AppRouter extends RootStackRouter { page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftBackupRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftBackupAlbumSelectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), AutoRoute( page: LocalTimelineRoute.page, guards: [_authGuard, _duplicateGuard], @@ -425,7 +439,7 @@ class AppRouter extends RootStackRouter { ), AutoRoute( page: DriftLockedFolderRoute.page, - guards: [_authGuard, _duplicateGuard], + guards: [_authGuard, _lockedGuard, _duplicateGuard], ), AutoRoute( page: DriftVideoRoute.page, @@ -463,6 +477,24 @@ class AppRouter extends RootStackRouter { page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftUserSelectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + + AutoRoute( + page: ChangeExperienceRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + + AutoRoute( + page: DriftPartnerRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftUploadDetailRoute.page, + guards: [_authGuard, _duplicateGuard], + ), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index e4719697c2..0e24f776d8 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -503,6 +503,49 @@ class BackupOptionsRoute extends PageRouteInfo { ); } +/// generated route for +/// [ChangeExperiencePage] +class ChangeExperienceRoute extends PageRouteInfo { + ChangeExperienceRoute({ + Key? key, + required bool switchingToBeta, + List? children, + }) : super( + ChangeExperienceRoute.name, + args: ChangeExperienceRouteArgs( + key: key, + switchingToBeta: switchingToBeta, + ), + initialChildren: children, + ); + + static const String name = 'ChangeExperienceRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return ChangeExperiencePage( + key: args.key, + switchingToBeta: args.switchingToBeta, + ); + }, + ); +} + +class ChangeExperienceRouteArgs { + const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta}); + + final Key? key; + + final bool switchingToBeta; + + @override + String toString() { + return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}'; + } +} + /// generated route for /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { @@ -683,6 +726,38 @@ class DriftAssetSelectionTimelineRouteArgs { } } +/// generated route for +/// [DriftBackupAlbumSelectionPage] +class DriftBackupAlbumSelectionRoute extends PageRouteInfo { + const DriftBackupAlbumSelectionRoute({List? children}) + : super(DriftBackupAlbumSelectionRoute.name, initialChildren: children); + + static const String name = 'DriftBackupAlbumSelectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftBackupAlbumSelectionPage(); + }, + ); +} + +/// generated route for +/// [DriftBackupPage] +class DriftBackupRoute extends PageRouteInfo { + const DriftBackupRoute({List? children}) + : super(DriftBackupRoute.name, initialChildren: children); + + static const String name = 'DriftBackupRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftBackupPage(); + }, + ); +} + /// generated route for /// [DriftCreateAlbumPage] class DriftCreateAlbumRoute extends PageRouteInfo { @@ -821,7 +896,7 @@ class DriftPartnerDetailRoute extends PageRouteInfo { DriftPartnerDetailRoute({ Key? key, - required UserDto partner, + required PartnerUserDto partner, List? children, }) : super( DriftPartnerDetailRoute.name, @@ -845,7 +920,7 @@ class DriftPartnerDetailRouteArgs { final Key? key; - final UserDto partner; + final PartnerUserDto partner; @override String toString() { @@ -853,6 +928,22 @@ class DriftPartnerDetailRouteArgs { } } +/// generated route for +/// [DriftPartnerPage] +class DriftPartnerRoute extends PageRouteInfo { + const DriftPartnerRoute({List? children}) + : super(DriftPartnerRoute.name, initialChildren: children); + + static const String name = 'DriftPartnerRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftPartnerPage(); + }, + ); +} + /// generated route for /// [DriftPlaceDetailPage] class DriftPlaceDetailRoute extends PageRouteInfo { @@ -948,6 +1039,45 @@ class DriftRecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftSearchPage] +class DriftSearchRoute extends PageRouteInfo { + DriftSearchRoute({ + Key? key, + SearchFilter? preFilter, + List? children, + }) : super( + DriftSearchRoute.name, + args: DriftSearchRouteArgs(key: key, preFilter: preFilter), + initialChildren: children, + ); + + static const String name = 'DriftSearchRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const DriftSearchRouteArgs(), + ); + return DriftSearchPage(key: args.key, preFilter: args.preFilter); + }, + ); +} + +class DriftSearchRouteArgs { + const DriftSearchRouteArgs({this.key, this.preFilter}); + + final Key? key; + + final SearchFilter? preFilter; + + @override + String toString() { + return 'DriftSearchRouteArgs{key: $key, preFilter: $preFilter}'; + } +} + /// generated route for /// [DriftTrashPage] class DriftTrashRoute extends PageRouteInfo { @@ -964,6 +1094,60 @@ class DriftTrashRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftUploadDetailPage] +class DriftUploadDetailRoute extends PageRouteInfo { + const DriftUploadDetailRoute({List? children}) + : super(DriftUploadDetailRoute.name, initialChildren: children); + + static const String name = 'DriftUploadDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftUploadDetailPage(); + }, + ); +} + +/// generated route for +/// [DriftUserSelectionPage] +class DriftUserSelectionRoute + extends PageRouteInfo { + DriftUserSelectionRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + DriftUserSelectionRoute.name, + args: DriftUserSelectionRouteArgs(key: key, album: album), + initialChildren: children, + ); + + static const String name = 'DriftUserSelectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftUserSelectionPage(key: args.key, album: args.album); + }, + ); +} + +class DriftUserSelectionRouteArgs { + const DriftUserSelectionRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'DriftUserSelectionRouteArgs{key: $key, album: $album}'; + } +} + /// generated route for /// [DriftVideoPage] class DriftVideoRoute extends PageRouteInfo { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index d7c625b981..7b0d74e420 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,38 +1,51 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionServiceProvider = Provider( (ref) => ActionService( ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider), + ref.watch(localAssetRepository), ref.watch(driftAlbumApiRepositoryProvider), ref.watch(remoteAlbumRepository), + ref.watch(assetMediaRepositoryProvider), + ref.watch(downloadRepositoryProvider), ), ); class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; + final DriftLocalAssetRepository _localAssetRepository; final DriftAlbumApiRepository _albumApiRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository; + final AssetMediaRepository _assetMediaRepository; + final DownloadRepository _downloadRepository; const ActionService( this._assetApiRepository, this._remoteAssetRepository, + this._localAssetRepository, this._albumApiRepository, this._remoteAlbumRepository, + this._assetMediaRepository, + this._downloadRepository, ); Future shareLink(List remoteIds, BuildContext context) async { @@ -75,7 +88,10 @@ class ActionService { ); } - Future moveToLockFolder(List remoteIds) async { + Future moveToLockFolder( + List remoteIds, + List localIds, + ) async { await _assetApiRepository.updateVisibility( remoteIds, AssetVisibilityEnum.locked, @@ -84,6 +100,15 @@ class ActionService { remoteIds, AssetVisibility.locked, ); + + // Ask user if they want to delete local copies + if (localIds.isNotEmpty) { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + } + } } Future removeFromLockFolder(List remoteIds) async { @@ -107,16 +132,21 @@ class ActionService { await _remoteAssetRepository.delete(remoteIds); } + Future deleteLocal(List localIds) async { + await _assetMediaRepository.deleteAll(localIds); + await _localAssetRepository.delete(localIds); + } + Future editLocation( List remoteIds, BuildContext context, ) async { - LatLng? initialLatLng; + maplibre.LatLng? initialLatLng; if (remoteIds.length == 1) { final exif = await _remoteAssetRepository.getExif(remoteIds[0]); if (exif?.latitude != null && exif?.longitude != null) { - initialLatLng = LatLng(exif!.latitude!, exif.longitude!); + initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!); } } @@ -152,4 +182,22 @@ class ActionService { return removedCount; } + + Future stack(String userId, List remoteIds) async { + final stack = await _assetApiRepository.stack(remoteIds); + await _remoteAssetRepository.stack(userId, stack); + } + + Future unStack(List stackIds) async { + await _remoteAssetRepository.unStack(stackIds); + await _assetApiRepository.unStack(stackIds); + } + + Future shareAssets(List assets) { + return _assetMediaRepository.shareAssets(assets); + } + + Future> downloadAll(List assets) { + return _downloadRepository.downloadAllAssets(assets); + } } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 8d1f7c24a5..cefc52385a 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -90,6 +90,8 @@ enum AppSettingsEnum { null, true, ), + betaTimeline(StoreKey.betaTimeline, null, false), + enableBackup(StoreKey.enableBackup, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/drift_backup.service.dart b/mobile/lib/services/drift_backup.service.dart new file mode 100644 index 0000000000..2f51c261fb --- /dev/null +++ b/mobile/lib/services/drift_backup.service.dart @@ -0,0 +1,293 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; +import 'package:immich_mobile/services/upload.service.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +final driftBackupServiceProvider = Provider( + (ref) => DriftBackupService( + ref.watch(backupRepositoryProvider), + ref.watch(storageRepositoryProvider), + ref.watch(uploadServiceProvider), + ref.watch(localAssetRepository), + ), +); + +class DriftBackupService { + DriftBackupService( + this._backupRepository, + this._storageRepository, + this._uploadService, + this._localAssetRepository, + ) { + _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate); + } + + final DriftBackupRepository _backupRepository; + final StorageRepository _storageRepository; + final DriftLocalAssetRepository _localAssetRepository; + final UploadService _uploadService; + final _log = Logger("DriftBackupService"); + + bool shouldCancel = false; + + Future getTotalCount() { + return _backupRepository.getTotalCount(); + } + + Future getRemainderCount() { + return _backupRepository.getRemainderCount(); + } + + Future getBackupCount() { + return _backupRepository.getBackupCount(); + } + + Future backup( + void Function(EnqueueStatus status) onEnqueueTasks, + ) async { + shouldCancel = false; + + final candidates = await _backupRepository.getCandidates(); + if (candidates.isEmpty) { + return; + } + + const batchSize = 100; + int count = 0; + for (int i = 0; i < candidates.length; i += batchSize) { + if (shouldCancel) { + break; + } + + final batch = candidates.skip(i).take(batchSize).toList(); + + List tasks = []; + for (final asset in batch) { + final task = await _getUploadTask(asset); + if (task != null) { + tasks.add(task); + } + } + + if (tasks.isNotEmpty && !shouldCancel) { + count += tasks.length; + _uploadService.enqueueTasks(tasks); + + onEnqueueTasks( + EnqueueStatus( + enqueueCount: count, + totalCount: candidates.length, + ), + ); + } + } + } + + void _handleTaskStatusUpdate(TaskStatusUpdate update) { + switch (update.status) { + case TaskStatus.complete: + _handleLivePhoto(update); + break; + + default: + break; + } + } + + Future _handleLivePhoto(TaskStatusUpdate update) async { + try { + if (update.task.metaData.isEmpty || update.task.metaData == '') { + return; + } + + final metadata = UploadTaskMetadata.fromJson(update.task.metaData); + if (!metadata.isLivePhotos) { + return; + } + + if (update.responseBody == null || update.responseBody!.isEmpty) { + return; + } + final response = jsonDecode(update.responseBody!); + + final localAsset = + await _localAssetRepository.getById(metadata.localAssetId); + if (localAsset == null) { + return; + } + + final uploadTask = await _getLivePhotoUploadTask( + localAsset, + response['id'] as String, + ); + + if (uploadTask == null) { + return; + } + + _uploadService.enqueueTasks([uploadTask]); + } catch (error, stackTrace) { + _log.severe("Error handling live photo upload task", error, stackTrace); + debugPrint("Error handling live photo upload task: $error $stackTrace"); + } + } + + Future _getUploadTask(LocalAsset asset) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + File? file; + + /// iOS LivePhoto has two files: a photo and a video. + /// They are uploaded separately, with video file being upload first, then returned with the assetId + /// The assetId is then used as a metadata for the photo file upload task. + /// + /// We implement two separate upload groups for this, the normal one for the video file + /// and the higher priority group for the photo file because the video file is already uploaded. + /// + /// The cancel operation will only cancel the video group (normal group), the photo group will not + /// be touched, as the video file is already uploaded. + + if (entity.isLivePhoto) { + file = await _storageRepository.getMotionFileForAsset(asset); + } else { + file = await _storageRepository.getFileForAsset(asset.id); + } + + if (file == null) { + return null; + } + + final originalFileName = entity.isLivePhoto + ? p.setExtension( + asset.name, + p.extension(file.path), + ) + : asset.name; + + String metadata = UploadTaskMetadata( + localAssetId: asset.id, + isLivePhotos: entity.isLivePhoto, + livePhotoVideoId: '', + ).toJson(); + + return _uploadService.buildUploadTask( + file, + originalFileName: originalFileName, + deviceAssetId: asset.id, + metadata: metadata, + group: kBackupGroup, + ); + } + + Future _getLivePhotoUploadTask( + LocalAsset asset, + String livePhotoVideoId, + ) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + final file = await _storageRepository.getFileForAsset(asset.id); + if (file == null) { + return null; + } + + final fields = { + 'livePhotoVideoId': livePhotoVideoId, + }; + + return _uploadService.buildUploadTask( + file, + originalFileName: asset.name, + deviceAssetId: asset.id, + fields: fields, + group: kBackupLivePhotoGroup, + priority: 0, // Highest priority to get upload immediately + ); + } + + Future cancel() async { + shouldCancel = true; + await _uploadService.cancelAllForGroup(kBackupGroup); + } +} + +class UploadTaskMetadata { + final String localAssetId; + final bool isLivePhotos; + final String livePhotoVideoId; + + const UploadTaskMetadata({ + required this.localAssetId, + required this.isLivePhotos, + required this.livePhotoVideoId, + }); + + UploadTaskMetadata copyWith({ + String? localAssetId, + bool? isLivePhotos, + String? livePhotoVideoId, + }) { + return UploadTaskMetadata( + localAssetId: localAssetId ?? this.localAssetId, + isLivePhotos: isLivePhotos ?? this.isLivePhotos, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + ); + } + + Map toMap() { + return { + 'localAssetId': localAssetId, + 'isLivePhotos': isLivePhotos, + 'livePhotoVideoId': livePhotoVideoId, + }; + } + + factory UploadTaskMetadata.fromMap(Map map) { + return UploadTaskMetadata( + localAssetId: map['localAssetId'] as String, + isLivePhotos: map['isLivePhotos'] as bool, + livePhotoVideoId: map['livePhotoVideoId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory UploadTaskMetadata.fromJson(String source) => + UploadTaskMetadata.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'UploadTaskMetadata(localAssetId: $localAssetId, isLivePhotos: $isLivePhotos, livePhotoVideoId: $livePhotoVideoId)'; + + @override + bool operator ==(covariant UploadTaskMetadata other) { + if (identical(this, other)) return true; + + return other.localAssetId == localAssetId && + other.isLivePhotos == isLivePhotos && + other.livePhotoVideoId == livePhotoVideoId; + } + + @override + int get hashCode => + localAssetId.hashCode ^ isLivePhotos.hashCode ^ livePhotoVideoId.hashCode; +} diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart index 5a8c27b0db..6d6646fe50 100644 --- a/mobile/lib/services/gcast.service.dart +++ b/mobile/lib/services/gcast.service.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:cast/session.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/models/sessions/session_create_response.model.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; @@ -156,12 +156,10 @@ class GCastService { return bufferedExpiration.isAfter(DateTime.now()); } - void loadMedia(Asset asset, bool reload) async { + void loadMedia(RemoteAsset asset, bool reload) async { if (!isConnected) { return; - } else if (asset.remoteId == null) { - return; - } else if (asset.remoteId == currentAssetId && !reload) { + } else if (asset.id == currentAssetId && !reload) { return; } @@ -176,10 +174,10 @@ class GCastService { final unauthenticatedUrl = asset.isVideo ? getPlaybackUrlForRemoteId( - asset.remoteId!, + asset.id, ) : getThumbnailUrlForRemoteId( - asset.remoteId!, + asset.id, type: AssetMediaSize.fullsize, ); @@ -187,8 +185,7 @@ class GCastService { "$unauthenticatedUrl&sessionKey=${sessionKey?.token}"; // get image mime type - final mimeType = - await _assetApiRepository.getAssetMIMEType(asset.remoteId!); + final mimeType = await _assetApiRepository.getAssetMIMEType(asset.id); if (mimeType == null) { return; @@ -205,7 +202,7 @@ class GCastService { "autoplay": true, }); - currentAssetId = asset.remoteId; + currentAssetId = asset.id; // we need to poll for media status since the cast device does not // send a message when the media is loaded for whatever reason diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index a591ad4f27..08b18dfd10 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -28,7 +28,7 @@ class PersonService { this._assetRepository, ); - Future> getAllPeople() async { + Future> getAllPeople() async { try { return await _personApiRepository.getAll(); } catch (error, stack) { @@ -48,7 +48,7 @@ class PersonService { return []; } - Future updateName(String id, String name) async { + Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); } catch (error, stack) { diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 5d3b08aaed..aa72a7908b 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.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:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; @@ -14,15 +15,21 @@ final searchServiceProvider = Provider( (ref) => SearchService( ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider), + ref.watch(searchApiRepositoryProvider), ), ); class SearchService { final ApiService _apiService; final AssetRepository _assetRepository; + final SearchApiRepository _searchApiRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._assetRepository); + SearchService( + this._apiService, + this._assetRepository, + this._searchApiRepository, + ); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -32,7 +39,7 @@ class SearchService { String? model, }) async { try { - return await _apiService.searchApi.getSearchSuggestions( + return await _searchApiRepository.getSearchSuggestions( type, country: country, state: state, @@ -47,76 +54,15 @@ class SearchService { Future search(SearchFilter filter, int page) async { try { - SearchResponseDto? response; - AssetTypeEnum? type; - if (filter.mediaType == AssetType.image) { - type = AssetTypeEnum.IMAGE; - } else if (filter.mediaType == AssetType.video) { - type = AssetTypeEnum.VIDEO; - } - - if (filter.context != null && filter.context!.isNotEmpty) { - response = await _apiService.searchApi.searchSmart( - SmartSearchDto( - query: filter.context!, - language: filter.language, - country: filter.location.country, - state: filter.location.state, - city: filter.location.city, - make: filter.camera.make, - model: filter.camera.model, - takenAfter: filter.date.takenAfter, - takenBefore: filter.date.takenBefore, - visibility: filter.display.isArchive - ? AssetVisibility.archive - : AssetVisibility.timeline, - isFavorite: filter.display.isFavorite ? true : null, - isNotInAlbum: filter.display.isNotInAlbum ? true : null, - personIds: filter.people.map((e) => e.id).toList(), - type: type, - page: page, - size: 1000, - ), - ); - } else { - response = await _apiService.searchApi.searchAssets( - MetadataSearchDto( - originalFileName: - filter.filename != null && filter.filename!.isNotEmpty - ? filter.filename - : null, - country: filter.location.country, - description: - filter.description != null && filter.description!.isNotEmpty - ? filter.description - : null, - state: filter.location.state, - city: filter.location.city, - make: filter.camera.make, - model: filter.camera.model, - takenAfter: filter.date.takenAfter, - takenBefore: filter.date.takenBefore, - visibility: filter.display.isArchive - ? AssetVisibility.archive - : AssetVisibility.timeline, - isFavorite: filter.display.isFavorite ? true : null, - isNotInAlbum: filter.display.isNotInAlbum ? true : null, - personIds: filter.people.map((e) => e.id).toList(), - type: type, - page: page, - size: 1000, - ), - ); - } + final response = await _searchApiRepository.search(filter, page); if (response == null || response.assets.items.isEmpty) { return null; } return SearchResult( - assets: await _assetRepository.getAllByRemoteId( - response.assets.items.map((e) => e.id), - ), + assets: await _assetRepository + .getAllByRemoteId(response.assets.items.map((e) => e.id)), nextPage: response.assets.nextPage?.toInt(), ); } catch (error, stackTrace) { diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index 18f90ab844..b869624e52 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; @@ -6,22 +7,28 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/upload.dart'; import 'package:path/path.dart'; -// import 'package:logging/logging.dart'; -final uploadServiceProvider = Provider( - (ref) => UploadService( - ref.watch(uploadRepositoryProvider), - ), -); +final uploadServiceProvider = Provider((ref) { + final service = UploadService(ref.watch(uploadRepositoryProvider)); + ref.onDispose(service.dispose); + return service; +}); class UploadService { final UploadRepository _uploadRepository; - // final Logger _log = Logger("UploadService"); void Function(TaskStatusUpdate)? onUploadStatus; void Function(TaskProgressUpdate)? onTaskProgress; + final StreamController _taskStatusController = + StreamController.broadcast(); + final StreamController _taskProgressController = + StreamController.broadcast(); + + Stream get taskStatusStream => _taskStatusController.stream; + Stream get taskProgressStream => + _taskProgressController.stream; + UploadService( this._uploadRepository, ) { @@ -31,29 +38,65 @@ class UploadService { void _onTaskProgressCallback(TaskProgressUpdate update) { onTaskProgress?.call(update); + if (!_taskProgressController.isClosed) { + _taskProgressController.add(update); + } } void _onUploadCallback(TaskStatusUpdate update) { onUploadStatus?.call(update); + if (!_taskStatusController.isClosed) { + _taskStatusController.add(update); + } + } + + void dispose() { + _taskStatusController.close(); + _taskProgressController.close(); } Future cancelUpload(String id) { return FileDownloader().cancelTaskWithId(id); } - Future upload(File file) async { - final task = await _buildUploadTask( - hash(file.path).toString(), - file, - ); - - await _uploadRepository.upload(task); + Future cancelAllForGroup(String group) async { + await _uploadRepository.cancelAll(group); + await _uploadRepository.reset(group); + await _uploadRepository.deleteAllTrackingRecords(group); } - Future _buildUploadTask( + void enqueueTasks(List tasks) { + _uploadRepository.enqueueAll(tasks); + } + + Future buildUploadTask( + File file, { + required String group, + Map? fields, + String? originalFileName, + String? deviceAssetId, + String? metadata, + int? priority, + }) async { + return _buildTask( + deviceAssetId ?? hash(file.path).toString(), + file, + fields: fields, + originalFileName: originalFileName, + metadata: metadata, + group: group, + priority: priority, + ); + } + + Future _buildTask( String id, File file, { + required String group, Map? fields, + String? originalFileName, + String? metadata, + int? priority, }) async { final serverEndpoint = Store.get(StoreKey.serverEndpoint); final url = Uri.parse('$serverEndpoint/assets').toString(); @@ -65,9 +108,8 @@ class UploadService { final stats = await file.stat(); final fileCreatedAt = stats.changed; final fileModifiedAt = stats.modified; - final fieldsMap = { - 'filename': filename, + 'filename': originalFileName ?? filename, 'deviceAssetId': id, 'deviceId': deviceId, 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), @@ -79,6 +121,7 @@ class UploadService { return UploadTask( taskId: id, + displayName: originalFileName ?? filename, httpRequestMethod: 'POST', url: url, headers: headers, @@ -87,8 +130,11 @@ class UploadService { baseDirectory: baseDirectory, directory: directory, fileField: 'assetData', - group: uploadGroup, + metaData: metadata ?? '', + group: group, + priority: priority ?? 5, updates: Updates.statusAndProgress, + retries: 3, ); } } diff --git a/mobile/lib/services/widget.service.dart b/mobile/lib/services/widget.service.dart index 02ddedbe89..fb2022784f 100644 --- a/mobile/lib/services/widget.service.dart +++ b/mobile/lib/services/widget.service.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/repositories/widget.repository.dart'; @@ -33,10 +32,8 @@ class WidgetService { } Future refreshWidgets() async { - if (Platform.isAndroid) return; - - for (final name in kWidgetNames) { - await _repository.refresh(name); + for (final (iOSName, androidName) in kWidgetNames) { + await _repository.refresh(iOSName, androidName); } } } diff --git a/mobile/lib/utils/database.utils.dart b/mobile/lib/utils/database.utils.dart new file mode 100644 index 0000000000..446b92db19 --- /dev/null +++ b/mobile/lib/utils/database.utils.dart @@ -0,0 +1,31 @@ +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; + +extension LocalAlbumEntityDataHelper on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + ); + } +} + +extension LocalAssetEntityDataHelper on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + ); + } +} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index 6b20fa7f37..3c2aeed756 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; @@ -59,9 +60,28 @@ Cancelable runInIsolateGentle({ stack, ); } finally { - await LogService.I.flushBuffer(); - ref.read(driftProvider).close(); - ref.read(isarProvider).close(); + try { + await LogService.I.flushBuffer(); + await ref.read(driftProvider).close(); + + // Close Isar safely + try { + final isar = ref.read(isarProvider); + if (isar.isOpen) { + await isar.close(); + } + } catch (e) { + debugPrint("Error closing Isar: $e"); + } + + ref.dispose(); + } catch (error) { + debugPrint("Error closing resources in isolate: $error"); + } finally { + ref.dispose(); + // Delay to ensure all resources are released + await Future.delayed(const Duration(seconds: 2)); + } } return null; }); diff --git a/mobile/lib/utils/licenses.dart b/mobile/lib/utils/licenses.dart new file mode 100644 index 0000000000..5ebc2c7b1a --- /dev/null +++ b/mobile/lib/utils/licenses.dart @@ -0,0 +1,42 @@ +const nonPubLicenses = { + 'aves': ''' +BSD 3-Clause License + +Copyright (c) 2020, Thibault Deckers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''', + 'photo_view': ''' +Copyright 2024 Renan C. Araújo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''', +}; diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 3ec8ce5bbc..a95c376ac2 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -2,24 +2,32 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart' + as isar_backup_album; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; @@ -48,14 +56,6 @@ Future migrateDatabaseIfNeeded(Isar db) async { await _migrateDeviceAsset(db); } - if (version < 12 && (!kReleaseMode)) { - final backgroundSync = BackgroundSyncManager(); - await backgroundSync.syncLocal(); - final drift = Drift(); - await _migrateDeviceAssetToSqlite(db, drift); - await drift.close(); - } - if (version < 13) { await Store.put(StoreKey.photoManagerCustomFilter, true); } @@ -175,33 +175,89 @@ Future _migrateDeviceAsset(Isar db) async { }); } -Future _migrateDeviceAssetToSqlite(Isar db, Drift drift) async { +Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { - final isarDeviceAssets = - await db.deviceAssetEntitys.where().sortByAssetId().findAll(); + final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); await drift.batch((batch) { for (final deviceAsset in isarDeviceAssets) { - final companion = LocalAssetEntityCompanion( - updatedAt: Value(deviceAsset.modifiedTime), - id: Value(deviceAsset.assetId), - checksum: Value(base64.encode(deviceAsset.hash)), - ); - batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( + batch.update( drift.localAssetEntity, - companion, - onConflict: DoUpdate( - (_) => companion, - where: (old) => old.updatedAt.equals(deviceAsset.modifiedTime), + LocalAssetEntityCompanion( + checksum: Value(base64.encode(deviceAsset.hash)), ), + where: (t) => t.id.equals(deviceAsset.assetId), ); } }); } catch (error) { - if (kDebugMode) { - debugPrint( - "[MIGRATION] Error while migrating device assets to SQLite: $error", - ); + debugPrint( + "[MIGRATION] Error while migrating device assets to SQLite: $error", + ); + } +} + +Future migrateBackupAlbumsToSqlite( + Isar db, + Drift drift, +) async { + try { + final isarBackupAlbums = await db.backupAlbums.where().findAll(); + // Recents is a virtual album on Android, and we don't have it with the new sync + // If recents is selected previously, select all albums during migration except the excluded ones + if (Platform.isAndroid) { + final recentAlbum = + isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll'); + if (recentAlbum != null) { + await drift.localAlbumEntity.update().write( + const LocalAlbumEntityCompanion( + backupSelection: Value(BackupSelection.selected), + ), + ); + final excluded = isarBackupAlbums + .where( + (album) => + album.selection == isar_backup_album.BackupSelection.exclude, + ) + .map((album) => album.id) + .toList(); + await drift.batch((batch) async { + for (final id in excluded) { + batch.update( + drift.localAlbumEntity, + const LocalAlbumEntityCompanion( + backupSelection: Value(BackupSelection.excluded), + ), + where: (t) => t.id.equals(id), + ); + } + }); + } + return; } + + await drift.batch((batch) { + for (final album in isarBackupAlbums) { + batch.update( + drift.localAlbumEntity, + LocalAlbumEntityCompanion( + backupSelection: Value( + switch (album.selection) { + isar_backup_album.BackupSelection.none => BackupSelection.none, + isar_backup_album.BackupSelection.select => + BackupSelection.selected, + isar_backup_album.BackupSelection.exclude => + BackupSelection.excluded, + }, + ), + ), + where: (t) => t.id.equals(album.id), + ); + } + }); + } catch (error) { + debugPrint( + "[MIGRATION] Error while migrating backup albums to SQLite: $error", + ); } } @@ -212,3 +268,18 @@ class _DeviceAsset { const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); } + +Future runNewSync(WidgetRef ref, {bool full = false}) async { + ref.read(backupProvider.notifier).cancelBackup(); + + final backgroundManager = ref.read(backgroundSyncProvider); + Future.wait([ + backgroundManager.syncLocal(full: full).then( + (_) { + Logger("runNewSync").fine("Hashing assets after syncLocal"); + backgroundManager.hashAssets(); + }, + ), + backgroundManager.syncRemote(), + ]); +} diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart index bf18d24213..7af0e61d3c 100644 --- a/mobile/lib/utils/provider_utils.dart +++ b/mobile/lib/utils/provider_utils.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/infrastructure/search.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; @@ -15,4 +16,5 @@ void invalidateAllApiRepositoryProviders(WidgetRef ref) { ref.invalidate(personApiRepositoryProvider); ref.invalidate(assetApiRepositoryProvider); ref.invalidate(timelineRepositoryProvider); + ref.invalidate(searchApiRepositoryProvider); } diff --git a/mobile/lib/utils/upload.dart b/mobile/lib/utils/upload.dart deleted file mode 100644 index a0b77f1d93..0000000000 --- a/mobile/lib/utils/upload.dart +++ /dev/null @@ -1 +0,0 @@ -const uploadGroup = 'upload_group'; diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart new file mode 100644 index 0000000000..dd1a64abe0 --- /dev/null +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +class RemoteAlbumSharedUserIcons extends ConsumerWidget { + const RemoteAlbumSharedUserIcons({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + if (currentAlbum == null) { + return const SizedBox(); + } + + final sharedUsersAsync = + ref.watch(remoteAlbumSharedUsersProvider(currentAlbum.id)); + + return sharedUsersAsync.maybeWhen( + data: (sharedUsers) { + if (sharedUsers.isEmpty) { + return const SizedBox(); + } + + return SizedBox( + height: 50, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: UserCircleAvatar( + user: sharedUsers[index], + radius: 18, + size: 36, + hasBorder: true, + ), + ); + }), + itemCount: sharedUsers.length, + ), + ); + }, + orElse: () => const SizedBox(), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index d64e507170..18565c8332 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -76,7 +76,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget { if (asset == null) { return; } - ref.read(castProvider.notifier).loadMedia(asset, true); + ref.read(castProvider.notifier).loadMediaOld(asset, true); } return; } diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart new file mode 100644 index 0000000000..42178c972e --- /dev/null +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -0,0 +1,121 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class DriftAlbumInfoListTile extends HookConsumerWidget { + final LocalAlbum album; + + const DriftAlbumInfoListTile({super.key, required this.album}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bool isSelected = album.backupSelection == BackupSelection.selected; + final bool isExcluded = album.backupSelection == BackupSelection.excluded; + + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); + + buildTileColor() { + if (isSelected) { + return context.isDarkTheme + ? context.primaryColor.withAlpha(100) + : context.primaryColor.withAlpha(25); + } else if (isExcluded) { + return context.isDarkTheme + ? Colors.red[300]?.withAlpha(150) + : Colors.red[100]?.withAlpha(150); + } else { + return Colors.transparent; + } + } + + buildIcon() { + if (isSelected) { + return Icon( + Icons.check_circle_rounded, + color: context.colorScheme.primary, + ); + } + + if (isExcluded) { + return Icon( + Icons.remove_circle_rounded, + color: context.colorScheme.error, + ); + } + + return Icon( + Icons.circle, + color: context.colorScheme.surfaceContainerHighest, + ); + } + + return GestureDetector( + onDoubleTap: () { + ref.watch(hapticFeedbackProvider.notifier).selectionClick(); + + if (isExcluded) { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } else { + if (album.id == 'isAll' || album.name == 'Recents') { + ImmichToast.show( + context: context, + msg: 'Cannot exclude album contains all assets', + toastType: ToastType.error, + gravity: ToastGravity.BOTTOM, + ); + return; + } + + ref.read(backupAlbumProvider.notifier).excludeAlbum(album); + } + }, + child: ListTile( + tileColor: buildTileColor(), + contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + onTap: () { + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + if (isSelected) { + ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + } else { + ref.read(backupAlbumProvider.notifier).selectAlbum(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } + } + }, + leading: buildIcon(), + title: Text( + album.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(album.assetCount.toString()), + trailing: IconButton( + onPressed: () { + context.pushRoute(LocalTimelineRoute(album: album)); + }, + icon: Icon( + Icons.image_outlined, + color: context.primaryColor, + size: 24, + ), + splashRadius: 25, + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 9d8be7c6ce..388f202b6d 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -17,6 +17,8 @@ import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_server_info.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/widgets/common/immich_logo.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class ImmichAppBarDialog extends HookConsumerWidget { @@ -255,6 +257,28 @@ class ImmichAppBarDialog extends HookConsumerWidget { style: context.textTheme.bodySmall, ).tr(), ), + const SizedBox( + width: 20, + child: Text( + "•", + textAlign: TextAlign.center, + ), + ), + InkWell( + onTap: () async { + context.pop(); + final packageInfo = await PackageInfo.fromPlatform(); + showLicensePage( + context: context, + applicationIcon: const Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 10), + child: ImmichLogo(size: 40), + ), + applicationVersion: packageInfo.version, + ); + }, + child: Text("licenses", style: context.textTheme.bodySmall).tr(), + ), ], ), ); diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index c2c5b79753..c7ddeca6e0 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,15 +1,16 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -51,7 +52,6 @@ class ImmichSliverAppBar extends ConsumerWidget { pinned: pinned, snap: snap, expandedHeight: expandedHeight, - backgroundColor: context.colorScheme.surfaceContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(5), @@ -61,31 +61,6 @@ class ImmichSliverAppBar extends ConsumerWidget { centerTitle: false, title: title ?? const _ImmichLogoWithText(), actions: [ - if (actions != null) - ...actions!.map( - (action) => Padding( - padding: const EdgeInsets.only(right: 16), - child: action, - ), - ), - IconButton( - icon: const Icon(Icons.swipe_left_alt_rounded), - onPressed: () => context.pop(), - ), - IconButton( - onPressed: () { - ref.read(backgroundSyncProvider).syncLocal(full: true); - ref.read(backgroundSyncProvider).syncRemote(); - - Future.delayed( - const Duration(seconds: 10), - () => ref.read(backgroundSyncProvider).hashAssets(), - ); - }, - icon: const Icon( - Icons.sync, - ), - ), if (isCasting) Padding( padding: const EdgeInsets.only(right: 12), @@ -101,6 +76,19 @@ class ImmichSliverAppBar extends ConsumerWidget { ), ), ), + const _SyncStatusIndicator(), + if (actions != null) + ...actions!.map( + (action) => Padding( + padding: const EdgeInsets.only(right: 16), + child: action, + ), + ), + if (kDebugMode || kProfileMode) + IconButton( + icon: const Icon(Icons.science_rounded), + onPressed: () => context.pushRoute(const FeatInDevRoute()), + ), if (showUploadButton) const Padding( padding: EdgeInsets.only(right: 20), @@ -127,13 +115,30 @@ class _ImmichLogoWithText extends StatelessWidget { children: [ Builder( builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme - ? 'assets/immich-logo-inline-dark.svg' - : 'assets/immich-logo-inline-light.svg', - height: 40, + return Badge( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + backgroundColor: context.primaryColor, + alignment: Alignment.centerRight, + offset: const Offset(16, -8), + label: Text( + 'β', + style: TextStyle( + fontSize: 11, + color: context.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + fontFamily: 'OverpassMono', + height: 1.2, + ), + ), + child: Padding( + padding: const EdgeInsets.only(top: 3.0), + child: SvgPicture.asset( + context.isDarkTheme + ? 'assets/immich-logo-inline-dark.svg' + : 'assets/immich-logo-inline-light.svg', + height: 40, + ), ), ); }, @@ -206,7 +211,7 @@ class _BackupIndicator extends ConsumerWidget { final badgeBackground = context.colorScheme.surfaceContainer; return InkWell( - onTap: () => context.pushRoute(const BackupControllerRoute()), + onTap: () => context.pushRoute(const DriftBackupRoute()), borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( label: Container( @@ -276,3 +281,100 @@ class _BackupIndicator extends ConsumerWidget { return null; } } + +class _SyncStatusIndicator extends ConsumerStatefulWidget { + const _SyncStatusIndicator(); + + @override + ConsumerState<_SyncStatusIndicator> createState() => + _SyncStatusIndicatorState(); +} + +class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> + with TickerProviderStateMixin { + late AnimationController _rotationController; + late AnimationController _dismissalController; + late Animation _rotationAnimation; + late Animation _dismissalAnimation; + + @override + void initState() { + super.initState(); + _rotationController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + _dismissalController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(_rotationController); + _dismissalAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _dismissalController, + curve: Curves.easeOutQuart, + ), + ); + } + + @override + void dispose() { + _rotationController.dispose(); + _dismissalController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final syncStatus = ref.watch(syncStatusProvider); + final isSyncing = syncStatus.isRemoteSyncing; + + // Control animations based on sync status + if (isSyncing) { + if (!_rotationController.isAnimating) { + _rotationController.repeat(); + } + _dismissalController.reset(); + } else { + _rotationController.stop(); + if (_dismissalController.status == AnimationStatus.dismissed) { + _dismissalController.forward(); + } + } + + // Don't show anything if not syncing and dismissal animation is complete + if (!isSyncing && + _dismissalController.status == AnimationStatus.completed) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]), + builder: (context, child) { + return Padding( + padding: EdgeInsets.only(right: isSyncing ? 16 : 0), + child: Transform.scale( + scale: isSyncing ? 1.0 : _dismissalAnimation.value, + child: Opacity( + opacity: isSyncing ? 1.0 : _dismissalAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, + child: Icon( + Icons.sync, + size: 24, + color: context.primaryColor, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index faaccfa51a..eecc099a9e 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -21,7 +22,6 @@ class MesmerizingSliverAppBar extends ConsumerStatefulWidget { final String title; final IconData icon; - @override ConsumerState createState() => _MesmerizingSliverAppBarState(); diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart new file mode 100644 index 0000000000..41eed09d8c --- /dev/null +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -0,0 +1,688 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart'; + +class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { + const RemoteAlbumSliverAppBar({ + super.key, + this.icon = Icons.camera, + this.onShowOptions, + this.onToggleAlbumOrder, + this.onEditTitle, + }); + + final IconData icon; + final void Function()? onShowOptions; + final void Function()? onToggleAlbumOrder; + final void Function()? onEditTitle; + + @override + ConsumerState createState() => + _MesmerizingSliverAppBarState(); +} + +class _MesmerizingSliverAppBarState + extends ConsumerState { + double _scrollProgress = 0.0; + + double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { + if (settings?.maxExtent == null || settings?.minExtent == null) { + return 1.0; + } + + final deltaExtent = settings!.maxExtent - settings.minExtent; + if (deltaExtent <= 0.0) { + return 1.0; + } + + return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent) + .clamp(0.0, 1.0); + } + + @override + Widget build(BuildContext context) { + final isMultiSelectEnabled = + ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + if (currentAlbum == null) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + Color? actionIconColor = Color.lerp( + Colors.white, + context.primaryColor, + _scrollProgress, + ); + + List actionIconShadows = [ + if (_scrollProgress < 0.95) + Shadow( + offset: const Offset(0, 2), + blurRadius: 5, + color: Colors.black.withValues(alpha: 0.5), + ) + else + const Shadow( + offset: Offset(0, 2), + blurRadius: 0, + color: Colors.transparent, + ), + ]; + + return isMultiSelectEnabled + ? SliverToBoxAdapter( + child: switch (_scrollProgress) { + < 0.8 => const SizedBox(height: 120), + _ => const SizedBox(height: 452), + }, + ) + : SliverAppBar( + expandedHeight: 400.0, + floating: false, + pinned: true, + snap: false, + elevation: 0, + leading: IconButton( + icon: Icon( + Platform.isIOS + ? Icons.arrow_back_ios_new_rounded + : Icons.arrow_back, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: () { + ref.read(remoteAlbumProvider.notifier).refresh(); + context.pop(); + }, + ), + actions: [ + if (widget.onToggleAlbumOrder != null) + IconButton( + icon: Icon( + Icons.swap_vert_rounded, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: widget.onToggleAlbumOrder, + ), + if (widget.onShowOptions != null) + IconButton( + icon: Icon( + Icons.more_vert, + color: actionIconColor, + shadows: actionIconShadows, + ), + onPressed: widget.onShowOptions, + ), + ], + flexibleSpace: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType< + FlexibleSpaceBarSettings>(); + final scrollProgress = _calculateScrollProgress(settings); + + // Update scroll progress for the leading button + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollProgress != scrollProgress) { + setState(() { + _scrollProgress = scrollProgress; + }); + } + }); + + return FlexibleSpaceBar( + centerTitle: true, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + currentAlbum.name, + style: TextStyle( + color: context.primaryColor, + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ) + : null, + ), + background: _ExpandedBackground( + scrollProgress: scrollProgress, + icon: widget.icon, + onEditTitle: widget.onEditTitle, + ), + ); + }, + ), + ); + } +} + +class _ExpandedBackground extends ConsumerStatefulWidget { + final double scrollProgress; + final IconData icon; + final void Function()? onEditTitle; + + const _ExpandedBackground({ + required this.scrollProgress, + required this.icon, + this.onEditTitle, + }); + + @override + ConsumerState<_ExpandedBackground> createState() => + _ExpandedBackgroundState(); +} + +class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> + with SingleTickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 1.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + _slideController.forward(); + } + }); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final timelineService = ref.watch(timelineServiceProvider); + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + + if (currentAlbum == null) { + return const SizedBox.shrink(); + } + + final dateRange = ref.watch( + remoteAlbumDateRangeProvider(currentAlbum.id), + ); + return Stack( + fit: StackFit.expand, + children: [ + Transform.translate( + offset: Offset(0, widget.scrollProgress * 50), + child: Transform.scale( + scale: 1.4 - (widget.scrollProgress * 0.2), + child: _RandomAssetBackground( + timelineService: timelineService, + icon: widget.icon, + ), + ), + ), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: widget.scrollProgress * 2.0, + sigmaY: widget.scrollProgress * 2.0, + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.05), + Colors.transparent, + Colors.black.withValues(alpha: 0.3), + Colors.black.withValues( + alpha: 0.6 + (widget.scrollProgress * 0.25), + ), + ], + stops: const [0.0, 0.15, 0.55, 1.0], + ), + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + if (dateRange.hasValue) + Text( + DateRangeFormatting.formatDateRange( + dateRange.value!.$1.toLocal(), + dateRange.value!.$2.toLocal(), + context.locale, + ), + style: const TextStyle( + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ), + const Text( + " • ", + style: TextStyle( + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: const _ItemCountText(), + ), + ], + ), + GestureDetector( + onTap: widget.onEditTitle, + child: SizedBox( + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + currentAlbum.name, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black54, + ), + ], + ), + ), + ), + ), + ), + if (currentAlbum.description.isNotEmpty) + GestureDetector( + onTap: widget.onEditTitle, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 80, + ), + child: SingleChildScrollView( + child: Text( + currentAlbum.description, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 8, + color: Colors.black54, + ), + ], + ), + ), + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: RemoteAlbumSharedUserIcons(), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ItemCountText extends ConsumerStatefulWidget { + const _ItemCountText(); + + @override + ConsumerState<_ItemCountText> createState() => _ItemCountTextState(); +} + +class _ItemCountTextState extends ConsumerState<_ItemCountText> { + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = + EventStream.shared.listen((_) => setState(() {})); + } + + @override + void dispose() { + _reloadSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final assetCount = ref.watch( + timelineServiceProvider.select((s) => s.totalAssets), + ); + + return Text( + 'items_count'.t( + context: context, + args: {"count": assetCount}, + ), + style: context.textTheme.labelLarge?.copyWith( + color: Colors.white, + shadows: [ + const Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black87, + ), + ], + ), + ); + } +} + +class _RandomAssetBackground extends StatefulWidget { + final TimelineService timelineService; + final IconData icon; + + const _RandomAssetBackground({ + required this.timelineService, + required this.icon, + }); + + @override + State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState(); +} + +class _RandomAssetBackgroundState extends State<_RandomAssetBackground> + with TickerProviderStateMixin { + late AnimationController _zoomController; + late AnimationController _crossFadeController; + late Animation _zoomAnimation; + late Animation _panAnimation; + late Animation _crossFadeAnimation; + BaseAsset? _currentAsset; + BaseAsset? _nextAsset; + bool _isZoomingIn = true; + + @override + void initState() { + super.initState(); + + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + ); + + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _zoomAnimation = Tween( + begin: 1.0, + end: 1.2, + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _panAnimation = Tween( + begin: Offset.zero, + end: const Offset(0.5, -0.5), + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _crossFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _crossFadeController, + curve: Curves.easeInOutCubic, + ), + ); + + Future.delayed( + Durations.medium1, + () => _loadFirstAsset(), + ); + } + + @override + void dispose() { + _zoomController.dispose(); + _crossFadeController.dispose(); + super.dispose(); + } + + void _startAnimationCycle() { + if (_isZoomingIn) { + _zoomController.forward().then((_) { + _loadNextAsset(); + }); + } else { + _zoomController.reverse().then((_) { + _loadNextAsset(); + }); + } + } + + Future _loadFirstAsset() async { + if (!mounted) { + return; + } + + if (widget.timelineService.totalAssets == 0) { + setState(() { + _currentAsset = null; + }); + + return; + } + + setState(() { + _currentAsset = widget.timelineService.getRandomAsset(); + }); + + await _crossFadeController.forward(); + + if (_zoomController.status == AnimationStatus.dismissed) { + if (_isZoomingIn) { + _zoomController.reset(); + } else { + _zoomController.value = 1.0; + } + _startAnimationCycle(); + } + } + + Future _loadNextAsset() async { + if (!mounted) { + return; + } + + try { + if (widget.timelineService.totalAssets > 1) { + // Load next asset while keeping current one visible + final nextAsset = widget.timelineService.getRandomAsset(); + + setState(() { + _nextAsset = nextAsset; + }); + + await _crossFadeController.reverse(); + setState(() { + _currentAsset = _nextAsset; + _nextAsset = null; + }); + + _crossFadeController.value = 1.0; + + _isZoomingIn = !_isZoomingIn; + + _startAnimationCycle(); + } + } catch (e) { + _zoomController.reset(); + _startAnimationCycle(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.timelineService.totalAssets == 0) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge( + [_zoomAnimation, _panAnimation, _crossFadeAnimation], + ), + builder: (context, child) { + return Transform.scale( + scale: _zoomAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Transform.translate( + offset: _panAnimation.value, + filterQuality: Platform.isAndroid ? FilterQuality.low : null, + child: Stack( + fit: StackFit.expand, + children: [ + // Current image + if (_currentAsset != null) + Opacity( + opacity: _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_currentAsset!), + fit: BoxFit.cover, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return Container(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + + if (_nextAsset != null) + Opacity( + opacity: 1.0 - _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_nextAsset!), + fit: BoxFit.cover, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return const SizedBox.shrink(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index e8501f1184..479c30d6da 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -14,11 +14,13 @@ class UserCircleAvatar extends ConsumerWidget { final UserDto user; double radius; double size; + bool hasBorder; UserCircleAvatar({ super.key, this.radius = 22, this.size = 44, + this.hasBorder = false, required this.user, }); @@ -38,25 +40,39 @@ class UserCircleAvatar extends ConsumerWidget { ), child: Text(user.name[0].toUpperCase()), ); - return CircleAvatar( - backgroundColor: userAvatarColor, - radius: radius, - child: user.profileImagePath == null - ? textIcon - : ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(50)), - child: CachedNetworkImage( - fit: BoxFit.cover, - cacheKey: user.profileImagePath, - width: size, - height: size, - placeholder: (_, __) => Image.memory(kTransparentImage), - imageUrl: profileImageUrl, - httpHeaders: ApiService.getRequestHeaders(), - fadeInDuration: const Duration(milliseconds: 300), - errorWidget: (context, error, stackTrace) => textIcon, - ), - ), + return Tooltip( + message: user.name, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: hasBorder + ? Border.all( + color: Colors.grey[500]!, + width: 1, + ) + : null, + ), + child: CircleAvatar( + backgroundColor: userAvatarColor, + radius: radius, + child: user.profileImagePath == null + ? textIcon + : ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50)), + child: CachedNetworkImage( + fit: BoxFit.cover, + cacheKey: user.profileImagePath, + width: size, + height: size, + placeholder: (_, __) => Image.memory(kTransparentImage), + imageUrl: profileImageUrl, + httpHeaders: ApiService.getRequestHeaders(), + fadeInDuration: const Duration(milliseconds: 300), + errorWidget: (context, error, stackTrace) => textIcon, + ), + ), + ), + ), ); } } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 8ccd69930a..24a73b2cbc 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -17,6 +18,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/version_compatibility.dart'; @@ -192,6 +194,15 @@ class LoginForm extends HookConsumerWidget { if (result.shouldChangePassword && !result.isAdmin) { context.pushRoute(const ChangePasswordRoute()); } else { + final isBeta = Store.isBetaTimelineEnabled; + if (isBeta) { + await ref + .read(galleryPermissionNotifier.notifier) + .requestGalleryPermission(); + await runNewSync(ref); + context.replaceRoute(const TabShellRoute()); + return; + } context.replaceRoute(const TabControllerRoute()); } } catch (error) { @@ -292,9 +303,18 @@ class LoginForm extends HookConsumerWidget { if (isSuccess) { isLoading.value = false; final permission = ref.watch(galleryPermissionNotifier); - if (permission.isGranted || permission.isLimited) { + final isBeta = Store.isBetaTimelineEnabled; + if (!isBeta && (permission.isGranted || permission.isLimited)) { ref.watch(backupProvider.notifier).resumeBackup(); } + if (isBeta) { + await ref + .read(galleryPermissionNotifier.notifier) + .requestGalleryPermission(); + await runNewSync(ref); + context.replaceRoute(const TabShellRoute()); + return; + } context.replaceRoute(const TabControllerRoute()); } } catch (error, stack) { diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index 06935cd4b5..1e4b061be6 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -63,8 +63,14 @@ class MapThumbnail extends HookConsumerWidget { } Future onStyleLoaded() async { - if (showMarkerPin && controller.value != null) { - await controller.value?.addMarkerAtLatLng(centre); + try { + if (showMarkerPin && controller.value != null) { + await controller.value?.addMarkerAtLatLng(centre); + } + } finally { + // Calling methods on the controller after it is disposed will throw an error + // We do not have a way to check if the controller is disposed for now + // https://github.com/maplibre/flutter-maplibre-gl/issues/192 } styleLoaded.value = true; } diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 44d01d274e..05f699b44b 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -14,8 +14,8 @@ import 'package:immich_mobile/widgets/common/search_field.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,7 +24,7 @@ class PeoplePicker extends HookConsumerWidget { final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return Column( children: [ diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart new file mode 100644 index 0000000000..a9c873cb67 --- /dev/null +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -0,0 +1,278 @@ +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +class BetaTimelineListTile extends ConsumerStatefulWidget { + const BetaTimelineListTile({ + super.key, + }); + + @override + ConsumerState createState() => + _BetaTimelineListTileState(); +} + +class _BetaTimelineListTileState extends ConsumerState + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _rotationAnimation; + late Animation _pulseAnimation; + late Animation _gradientAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(seconds: 3), + vsync: this, + ); + + _rotationAnimation = Tween(begin: 0, end: 2 * math.pi).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.linear, + ), + ); + + _pulseAnimation = Tween(begin: 1, end: 1.1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _gradientAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _animationController.repeat(reverse: true); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final betaTimelineValue = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.betaTimeline); + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + void onSwitchChanged(bool value) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: value + ? const Text("Enable Beta Timeline") + : const Text("Disable Beta Timeline"), + content: value + ? const Text( + "Are you sure you want to enable the beta timeline?", + ) + : const Text( + "Are you sure you want to disable the beta timeline?", + ), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + "cancel".t(context: context), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: context.colorScheme.outline, + ), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + await ref.read(appSettingsServiceProvider).setSetting( + AppSettingsEnum.betaTimeline, + value, + ); + context.router.replaceAll( + [ChangeExperienceRoute(switchingToBeta: value)], + ); + }, + child: Text( + "ok".t(context: context), + ), + ), + ], + ); + }, + ); + } + + final gradientColors = [ + Color.lerp( + context.primaryColor.withValues(alpha: 0.5), + context.primaryColor.withValues(alpha: 0.3), + _gradientAnimation.value, + )!, + Color.lerp( + context.logoPink.withValues(alpha: 0.2), + context.logoPink.withValues(alpha: 0.4), + _gradientAnimation.value, + )!, + Color.lerp( + context.logoRed.withValues(alpha: 0.3), + context.logoRed.withValues(alpha: 0.5), + _gradientAnimation.value, + )!, + ]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + gradient: LinearGradient( + colors: gradientColors, + stops: const [0.0, 0.5, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + transform: GradientRotation(_rotationAnimation.value * 0.5), + ), + boxShadow: [ + BoxShadow( + color: context.primaryColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + color: context.scaffoldBackgroundColor, + ), + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(10.5)), + onTap: () => onSwitchChanged(!betaTimelineValue), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Transform.scale( + scale: _pulseAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 0.02, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.primaryColor.withValues(alpha: 0.2), + context.primaryColor.withValues(alpha: 0.1), + ], + ), + ), + child: Icon( + Icons.auto_awesome, + color: context.primaryColor, + size: 20, + ), + ), + ), + ), + const SizedBox(width: 28), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "advanced_settings_beta_timeline_title" + .t(context: context), + style: + context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + gradient: LinearGradient( + colors: [ + context.primaryColor + .withValues(alpha: 0.8), + context.primaryColor + .withValues(alpha: 0.6), + ], + ), + ), + child: Text( + 'NEW', + style: + context.textTheme.labelSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 10, + height: 1.2, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + "advanced_settings_beta_timeline_subtitle" + .t(context: context), + style: context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color + ?.withValues(alpha: 0.9), + ), + maxLines: 2, + ), + ], + ), + ), + Switch.adaptive( + value: betaTimelineValue, + onChanged: onSwitchChanged, + activeColor: context.primaryColor, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index becafa06bf..545955a184 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -257,6 +257,8 @@ part 'model/sync_album_user_v1.dart'; part 'model/sync_album_v1.dart'; part 'model/sync_asset_delete_v1.dart'; part 'model/sync_asset_exif_v1.dart'; +part 'model/sync_asset_face_delete_v1.dart'; +part 'model/sync_asset_face_v1.dart'; part 'model/sync_asset_v1.dart'; part 'model/sync_entity_type.dart'; part 'model/sync_memory_asset_delete_v1.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 603163f00e..55d6f4108b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -570,6 +570,10 @@ class ApiClient { return SyncAssetDeleteV1.fromJson(value); case 'SyncAssetExifV1': return SyncAssetExifV1.fromJson(value); + case 'SyncAssetFaceDeleteV1': + return SyncAssetFaceDeleteV1.fromJson(value); + case 'SyncAssetFaceV1': + return SyncAssetFaceV1.fromJson(value); case 'SyncAssetV1': return SyncAssetV1.fromJson(value); case 'SyncEntityType': diff --git a/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart new file mode 100644 index 0000000000..0992bfdcba --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetFaceDeleteV1 { + /// Returns a new [SyncAssetFaceDeleteV1] instance. + SyncAssetFaceDeleteV1({ + required this.assetFaceId, + }); + + String assetFaceId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceDeleteV1 && + other.assetFaceId == assetFaceId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetFaceId.hashCode); + + @override + String toString() => 'SyncAssetFaceDeleteV1[assetFaceId=$assetFaceId]'; + + Map toJson() { + final json = {}; + json[r'assetFaceId'] = this.assetFaceId; + return json; + } + + /// Returns a new [SyncAssetFaceDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceDeleteV1( + assetFaceId: mapValueOfType(json, r'assetFaceId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetFaceDeleteV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetFaceDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceDeleteV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetFaceDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetFaceId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart new file mode 100644 index 0000000000..853a8a1514 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -0,0 +1,175 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetFaceV1 { + /// Returns a new [SyncAssetFaceV1] instance. + SyncAssetFaceV1({ + required this.assetId, + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.id, + required this.imageHeight, + required this.imageWidth, + required this.personId, + required this.sourceType, + }); + + String assetId; + + num boundingBoxX1; + + num boundingBoxX2; + + num boundingBoxY1; + + num boundingBoxY2; + + String id; + + num imageHeight; + + num imageWidth; + + String? personId; + + String sourceType; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceV1 && + other.assetId == assetId && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth && + other.personId == personId && + other.sourceType == sourceType; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode) + + (personId == null ? 0 : personId!.hashCode) + + (sourceType.hashCode); + + @override + String toString() => 'SyncAssetFaceV1[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, personId=$personId, sourceType=$sourceType]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'boundingBoxX1'] = this.boundingBoxX1; + json[r'boundingBoxX2'] = this.boundingBoxX2; + json[r'boundingBoxY1'] = this.boundingBoxY1; + json[r'boundingBoxY2'] = this.boundingBoxY2; + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + if (this.personId != null) { + json[r'personId'] = this.personId; + } else { + // json[r'personId'] = null; + } + json[r'sourceType'] = this.sourceType; + return json; + } + + /// Returns a new [SyncAssetFaceV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceV1( + assetId: mapValueOfType(json, r'assetId')!, + boundingBoxX1: num.parse('${json[r'boundingBoxX1']}'), + boundingBoxX2: num.parse('${json[r'boundingBoxX2']}'), + boundingBoxY1: num.parse('${json[r'boundingBoxY1']}'), + boundingBoxY2: num.parse('${json[r'boundingBoxY2']}'), + id: mapValueOfType(json, r'id')!, + imageHeight: num.parse('${json[r'imageHeight']}'), + imageWidth: num.parse('${json[r'imageWidth']}'), + personId: mapValueOfType(json, r'personId'), + sourceType: mapValueOfType(json, r'sourceType')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetFaceV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetFaceV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetFaceV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'boundingBoxX1', + 'boundingBoxX2', + 'boundingBoxY1', + 'boundingBoxY2', + 'id', + 'imageHeight', + 'imageWidth', + 'personId', + 'sourceType', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 61f94401c7..65ed78105c 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -58,6 +58,8 @@ class SyncEntityType { static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1'); static const personV1 = SyncEntityType._(r'PersonV1'); static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); + static const assetFaceV1 = SyncEntityType._(r'AssetFaceV1'); + static const assetFaceDeleteV1 = SyncEntityType._(r'AssetFaceDeleteV1'); static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1'); static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); @@ -100,6 +102,8 @@ class SyncEntityType { stackDeleteV1, personV1, personDeleteV1, + assetFaceV1, + assetFaceDeleteV1, userMetadataV1, userMetadataDeleteV1, syncAckV1, @@ -177,6 +181,8 @@ class SyncEntityTypeTypeTransformer { case r'StackDeleteV1': return SyncEntityType.stackDeleteV1; case r'PersonV1': return SyncEntityType.personV1; case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; + case r'AssetFaceV1': return SyncEntityType.assetFaceV1; + case r'AssetFaceDeleteV1': return SyncEntityType.assetFaceDeleteV1; case r'UserMetadataV1': return SyncEntityType.userMetadataV1; case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'SyncAckV1': return SyncEntityType.syncAckV1; diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index e86c22f64b..6749beb3e1 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -22,7 +22,6 @@ class SyncPersonV1 { required this.isHidden, required this.name, required this.ownerId, - required this.thumbnailPath, required this.updatedAt, }); @@ -44,8 +43,6 @@ class SyncPersonV1 { String ownerId; - String thumbnailPath; - DateTime updatedAt; @override @@ -59,7 +56,6 @@ class SyncPersonV1 { other.isHidden == isHidden && other.name == name && other.ownerId == ownerId && - other.thumbnailPath == thumbnailPath && other.updatedAt == updatedAt; @override @@ -74,11 +70,10 @@ class SyncPersonV1 { (isHidden.hashCode) + (name.hashCode) + (ownerId.hashCode) + - (thumbnailPath.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SyncPersonV1[birthDate=$birthDate, color=$color, createdAt=$createdAt, faceAssetId=$faceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, ownerId=$ownerId, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]'; + String toString() => 'SyncPersonV1[birthDate=$birthDate, color=$color, createdAt=$createdAt, faceAssetId=$faceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, ownerId=$ownerId, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -103,7 +98,6 @@ class SyncPersonV1 { json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; - json[r'thumbnailPath'] = this.thumbnailPath; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); return json; } @@ -126,7 +120,6 @@ class SyncPersonV1 { isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!, ); } @@ -184,7 +177,6 @@ class SyncPersonV1 { 'isHidden', 'name', 'ownerId', - 'thumbnailPath', 'updatedAt', }; } diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 75ce852f9f..800b3f4485 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -39,6 +39,7 @@ class SyncRequestType { static const stacksV1 = SyncRequestType._(r'StacksV1'); static const usersV1 = SyncRequestType._(r'UsersV1'); static const peopleV1 = SyncRequestType._(r'PeopleV1'); + static const assetFacesV1 = SyncRequestType._(r'AssetFacesV1'); static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1'); /// List of all possible values in this [enum][SyncRequestType]. @@ -59,6 +60,7 @@ class SyncRequestType { stacksV1, usersV1, peopleV1, + assetFacesV1, userMetadataV1, ]; @@ -114,6 +116,7 @@ class SyncRequestTypeTypeTransformer { case r'StacksV1': return SyncRequestType.stacksV1; case r'UsersV1': return SyncRequestType.usersV1; case r'PeopleV1': return SyncRequestType.peopleV1; + case r'AssetFacesV1': return SyncRequestType.assetFacesV1; case r'UserMetadataV1': return SyncRequestType.userMetadataV1; default: if (!allowNull) { diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f8059d7e3c..bbffb9f51b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -139,7 +139,6 @@ flutter: - family: OverpassMono fonts: - asset: fonts/overpass/OverpassMono.ttf - flutter_launcher_icons: image_path_android: 'assets/immich-logo.png' adaptive_icon_background: '#ffffff' diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index c9fd8104e4..deb19dfcf8 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -105,6 +105,10 @@ void main() { .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any())) .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePeopleV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePeopleV1(any())) + .thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart new file mode 100644 index 0000000000..209e70d788 --- /dev/null +++ b/mobile/test/drift/main/generated/schema.dart @@ -0,0 +1,26 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; +import 'package:drift/internal/migrations.dart'; +import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + default: + throw MissingSchemaException(version, versions); + } + } + + static const versions = const [1, 2, 3]; +} diff --git a/mobile/test/drift/main/generated/schema_v1.dart b/mobile/test/drift/main/generated/schema_v1.dart new file mode 100644 index 0000000000..d7b88ea3cf --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v1.dart @@ -0,0 +1,5139 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn( + 'profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + id, + name, + isAdmin, + email, + profileImagePath, + updatedAt, + quotaSizeInBytes, + quotaUsageInBytes + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present + ? profileImagePath.value + : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present + ? quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present + ? data.profileImagePath.value + : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = + GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, + height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id)')); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = GeneratedColumn( + 'key', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData( + {required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith( + {String? userId, int? key, Uint8List? value}) => + UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith( + {Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData( + {required this.sharedById, + required this.sharedWithId, + required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith( + {String? sharedById, String? sharedWithId, bool? inTimeline}) => + PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: + data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: + data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith( + {Value? sharedById, + Value? sharedWithId, + Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = GeneratedColumn( + 'city', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = GeneratedColumn( + 'state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = GeneratedColumn( + 'country', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn('date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = GeneratedColumn( + 'make', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = GeneratedColumn( + 'model', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = GeneratedColumn( + 'order', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, createdAt, updatedAt, + ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData( + {required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith( + {String? albumId, String? userId, int? role}) => + RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith( + {Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, + type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith( + {Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn( + 'thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', aliasedName, true, + type: 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 + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements 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'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: + data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present + ? data.thumbnailPath.value + : this.thumbnailPath, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name, + faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV1 extends GeneratedDatabase { + DatabaseAtV1(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = Index('idx_remote_asset_checksum', + 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 1; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v2.dart b/mobile/test/drift/main/generated/schema_v2.dart new file mode 100644 index 0000000000..e3edac3501 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v2.dart @@ -0,0 +1,5139 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn( + 'profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + id, + name, + isAdmin, + email, + profileImagePath, + updatedAt, + quotaSizeInBytes, + quotaUsageInBytes + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present + ? profileImagePath.value + : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present + ? quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present + ? data.profileImagePath.value + : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = + GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, + height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id)')); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = GeneratedColumn( + 'key', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData( + {required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith( + {String? userId, int? key, Uint8List? value}) => + UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith( + {Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData( + {required this.sharedById, + required this.sharedWithId, + required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith( + {String? sharedById, String? sharedWithId, bool? inTimeline}) => + PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: + data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: + data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith( + {Value? sharedById, + Value? sharedWithId, + Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = GeneratedColumn( + 'city', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = GeneratedColumn( + 'state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = GeneratedColumn( + 'country', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn('date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = GeneratedColumn( + 'make', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = GeneratedColumn( + 'model', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = GeneratedColumn( + 'order', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, createdAt, updatedAt, + ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData( + {required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith( + {String? albumId, String? userId, int? role}) => + RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith( + {Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, + type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith( + {Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn( + 'thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', aliasedName, true, + type: 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 + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements 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'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: + data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present + ? data.thumbnailPath.value + : this.thumbnailPath, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name, + faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = Index('idx_remote_asset_checksum', + 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 2; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v3.dart b/mobile/test/drift/main/generated/schema_v3.dart new file mode 100644 index 0000000000..8f655b3f7d --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v3.dart @@ -0,0 +1,5136 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn profileImagePath = GeneratedColumn( + 'profile_image_path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + id, + name, + isAdmin, + email, + profileImagePath, + updatedAt, + quotaSizeInBytes, + quotaUsageInBytes + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + isAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_admin'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + profileImagePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}profile_image_path']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']), + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final bool isAdmin; + final String email; + final String? profileImagePath; + final DateTime updatedAt; + final int? quotaSizeInBytes; + final int quotaUsageInBytes; + const UserEntityData( + {required this.id, + required this.name, + required this.isAdmin, + required this.email, + this.profileImagePath, + required this.updatedAt, + this.quotaSizeInBytes, + required this.quotaUsageInBytes}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['is_admin'] = Variable(isAdmin); + map['email'] = Variable(email); + if (!nullToAbsent || profileImagePath != null) { + map['profile_image_path'] = Variable(profileImagePath); + } + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || quotaSizeInBytes != null) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + } + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + return map; + } + + factory UserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + isAdmin: serializer.fromJson(json['isAdmin']), + email: serializer.fromJson(json['email']), + profileImagePath: serializer.fromJson(json['profileImagePath']), + updatedAt: serializer.fromJson(json['updatedAt']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'isAdmin': serializer.toJson(isAdmin), + 'email': serializer.toJson(email), + 'profileImagePath': serializer.toJson(profileImagePath), + 'updatedAt': serializer.toJson(updatedAt), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + }; + } + + UserEntityData copyWith( + {String? id, + String? name, + bool? isAdmin, + String? email, + Value profileImagePath = const Value.absent(), + DateTime? updatedAt, + Value quotaSizeInBytes = const Value.absent(), + int? quotaUsageInBytes}) => + UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath.present + ? profileImagePath.value + : this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes.present + ? quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + email: data.email.present ? data.email.value : this.email, + profileImagePath: data.profileImagePath.present + ? data.profileImagePath.value + : this.profileImagePath, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.isAdmin == this.isAdmin && + other.email == this.email && + other.profileImagePath == this.profileImagePath && + other.updatedAt == this.updatedAt && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value isAdmin; + final Value email; + final Value profileImagePath; + final Value updatedAt; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.isAdmin = const Value.absent(), + this.email = const Value.absent(), + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + this.isAdmin = const Value.absent(), + required String email, + this.profileImagePath = const Value.absent(), + this.updatedAt = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? isAdmin, + Expression? email, + Expression? profileImagePath, + Expression? updatedAt, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (isAdmin != null) 'is_admin': isAdmin, + if (email != null) 'email': email, + if (profileImagePath != null) 'profile_image_path': profileImagePath, + if (updatedAt != null) 'updated_at': updatedAt, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + }); + } + + UserEntityCompanion copyWith( + {Value? id, + Value? name, + Value? isAdmin, + Value? email, + Value? profileImagePath, + Value? updatedAt, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes}) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + isAdmin: isAdmin ?? this.isAdmin, + email: email ?? this.email, + profileImagePath: profileImagePath ?? this.profileImagePath, + updatedAt: updatedAt ?? this.updatedAt, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (profileImagePath.present) { + map['profile_image_path'] = Variable(profileImagePath.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('isAdmin: $isAdmin, ') + ..write('email: $email, ') + ..write('profileImagePath: $profileImagePath, ') + ..write('updatedAt: $updatedAt, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn localDateTime = + GeneratedColumn('local_date_time', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}live_photo_video_id']), + visibility: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}visibility'])!, + stackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}stack_id']), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + }; + } + + RemoteAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent()}) => + RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + }); + } + + RemoteAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId}) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0')); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}orientation'])!, + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + }; + } + + LocalAssetEntityData copyWith( + {String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation}) => + LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, + height, durationInSeconds, id, checksum, isFavorite, orientation); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + }); + } + + LocalAssetEntityCompanion copyWith( + {Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation}) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => + [id, createdAt, updatedAt, ownerId, primaryAssetId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId}) => + StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId}) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn key = GeneratedColumn( + 'key', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + key: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}key'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}value'])!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData( + {required this.userId, required this.key, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith( + {String? userId, int? key, Uint8List? value}) => + UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith( + {Value? userId, Value? key, Value? value}) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("in_timeline" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, + sharedWithId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, + inTimeline: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData( + {required this.sharedById, + required this.sharedWithId, + required this.inTimeline}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith( + {String? sharedById, String? sharedWithId, bool? inTimeline}) => + PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: + data.sharedById.present ? data.sharedById.value : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: + data.inTimeline.present ? data.inTimeline.value : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith( + {Value? sharedById, + Value? sharedWithId, + Value? inTimeline}) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}backup_selection'])!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_ios_shared_album'])!, + marker_: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value marker_ = const Value.absent()}) => + LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, name, updatedAt, backupSelection, isIosSharedAlbum, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? marker_}) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + LocalAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn city = GeneratedColumn( + 'city', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn state = GeneratedColumn( + 'state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn country = GeneratedColumn( + 'country', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn('date_time_original', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn width = GeneratedColumn( + 'width', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn make = GeneratedColumn( + 'make', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn model = GeneratedColumn( + 'model', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}model']), + lens: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}lens']), + orientation: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith( + {String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent()}) => + RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith( + {Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType}) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('1')); + late final GeneratedColumn order = GeneratedColumn( + 'order', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}order'])!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order}) => + RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, createdAt, updatedAt, + ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order}) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith( + {Value? assetId, Value? albumId}) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}role'])!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData( + {required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith( + {String? albumId, String? userId, int? role}) => + RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith( + {Value? albumId, Value? userId, Value? role}) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const CustomExpression('0')); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}type'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData( + {required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent()}) => + MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId, + type, data, isSaved, memoryAt, seenAt, showAt, hideAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt}) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith( + {Value? assetId, Value? memoryId}) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn thumbnailPath = GeneratedColumn( + 'thumbnail_path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))')); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_hidden" IN (0, 1))')); + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', aliasedName, true, + type: 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 + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + faceAssetId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}face_asset_id']), + thumbnailPath: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!, + isFavorite: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + isHidden: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!, + color: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}color']), + birthDate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}birth_date']), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements 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'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['thumbnail_path'] = Variable(thumbnailPath); + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + thumbnailPath: serializer.fromJson(json['thumbnailPath']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'thumbnailPath': serializer.toJson(thumbnailPath), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + String? thumbnailPath, + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent()}) => + PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: + data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId, + thumbnailPath: data.thumbnailPath.present + ? data.thumbnailPath.value + : this.thumbnailPath, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name, + faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.thumbnailPath == this.thumbnailPath && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value thumbnailPath; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.thumbnailPath = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required String thumbnailPath, + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + thumbnailPath = Value(thumbnailPath), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? thumbnailPath, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (thumbnailPath != null) 'thumbnail_path': thumbnailPath, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith( + {Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? thumbnailPath, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate}) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (thumbnailPath.present) { + map['thumbnail_path'] = Variable(thumbnailPath.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('thumbnailPath: $thumbnailPath, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final Index idxLocalAssetChecksum = Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + late final Index uQRemoteAssetOwnerChecksum = Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + late final Index idxRemoteAssetChecksum = Index('idx_remote_asset_checksum', + 'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)'); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + localAssetEntity, + stackEntity, + idxLocalAssetChecksum, + uQRemoteAssetOwnerChecksum, + idxRemoteAssetChecksum, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAlbumAssetEntity, + remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity + ]; + @override + int get schemaVersion => 3; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/migration_test.dart b/mobile/test/drift/main/migration_test.dart new file mode 100644 index 0000000000..74467492ae --- /dev/null +++ b/mobile/test/drift/main/migration_test.dart @@ -0,0 +1,38 @@ +// dart format width=80 +// ignore_for_file: unused_local_variable, unused_import +import 'package:drift/drift.dart'; +import 'package:drift_dev/api/migrations_native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +import 'generated/schema.dart'; +import 'generated/schema_v1.dart' as v1; +import 'generated/schema_v2.dart' as v2; + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + group('simple database migrations', () { + // These simple tests verify all possible schema updates with a simple (no + // data) migration. This is a quick way to ensure that written database + // migrations properly alter the schema. + const versions = GeneratedHelper.versions; + for (final (i, fromVersion) in versions.indexed) { + group('from $fromVersion', () { + for (final toVersion in versions.skip(i + 1)) { + test('to $toVersion', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = Drift(schema.newConnection()); + await verifier.migrateAndValidate(db, toVersion); + await db.close(); + }); + } + }); + } + }); +} diff --git a/mobile/test/modules/extensions/datetime_extensions_test.dart b/mobile/test/modules/extensions/datetime_extensions_test.dart new file mode 100644 index 0000000000..412d946b1f --- /dev/null +++ b/mobile/test/modules/extensions/datetime_extensions_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; +import 'package:intl/date_symbol_data_local.dart'; + +void main() { + setUpAll(() async { + await initializeDateFormatting(); + }); + + group('DateRangeFormatting.formatDateRange', () { + final currentYear = DateTime.now().year; + + test('returns single date format for this year', () { + final date = DateTime(currentYear, 8, 28); // Aug 28 this year + final result = DateRangeFormatting.formatDateRange(date, date, null); + expect(result, 'Aug 28'); + }); + + test('returns single date format for other year', () { + final date = DateTime(2023, 8, 28); // Aug 28, 2023 + final result = DateRangeFormatting.formatDateRange(date, date, null); + expect(result, 'Aug 28, 2023'); + }); + + test('returns date range format for this year', () { + final startDate = DateTime(currentYear, 3, 23); // Mar 23 + final endDate = DateTime(currentYear, 5, 31); // May 31 + final result = + DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Mar 23 - May 31'); + }); + + test('returns date range format for other year (same year)', () { + final startDate = DateTime(2023, 8, 28); // Aug 28 + final endDate = DateTime(2023, 9, 30); // Sep 30 + final result = + DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Aug 28 - Sep 30, 2023'); + }); + + test('returns date range format over multiple years', () { + final startDate = DateTime(2021, 4, 17); // Apr 17, 2021 + final endDate = DateTime(2022, 4, 9); // Apr 9, 2022 + final result = + DateRangeFormatting.formatDateRange(startDate, endDate, null); + expect(result, 'Apr 17, 2021 - Apr 9, 2022'); + }); + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 11b516e626..5942c3447e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -13842,6 +13842,65 @@ ], "type": "object" }, + "SyncAssetFaceDeleteV1": { + "properties": { + "assetFaceId": { + "type": "string" + } + }, + "required": [ + "assetFaceId" + ], + "type": "object" + }, + "SyncAssetFaceV1": { + "properties": { + "assetId": { + "type": "string" + }, + "boundingBoxX1": { + "type": "number" + }, + "boundingBoxX2": { + "type": "number" + }, + "boundingBoxY1": { + "type": "number" + }, + "boundingBoxY2": { + "type": "number" + }, + "id": { + "type": "string" + }, + "imageHeight": { + "type": "number" + }, + "imageWidth": { + "type": "number" + }, + "personId": { + "nullable": true, + "type": "string" + }, + "sourceType": { + "type": "string" + } + }, + "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "id", + "imageHeight", + "imageWidth", + "personId", + "sourceType" + ], + "type": "object" + }, "SyncAssetV1": { "properties": { "checksum": { @@ -13966,6 +14025,8 @@ "StackDeleteV1", "PersonV1", "PersonDeleteV1", + "AssetFaceV1", + "AssetFaceDeleteV1", "UserMetadataV1", "UserMetadataDeleteV1", "SyncAckV1", @@ -14163,9 +14224,6 @@ "ownerId": { "type": "string" }, - "thumbnailPath": { - "type": "string" - }, "updatedAt": { "format": "date-time", "type": "string" @@ -14181,7 +14239,6 @@ "isHidden", "name", "ownerId", - "thumbnailPath", "updatedAt" ], "type": "object" @@ -14204,6 +14261,7 @@ "StacksV1", "UsersV1", "PeopleV1", + "AssetFacesV1", "UserMetadataV1" ], "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0d5daed14a..0ac41e0bca 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -4139,6 +4139,8 @@ export enum SyncEntityType { StackDeleteV1 = "StackDeleteV1", PersonV1 = "PersonV1", PersonDeleteV1 = "PersonDeleteV1", + AssetFaceV1 = "AssetFaceV1", + AssetFaceDeleteV1 = "AssetFaceDeleteV1", UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", SyncAckV1 = "SyncAckV1", @@ -4161,6 +4163,7 @@ export enum SyncRequestType { StacksV1 = "StacksV1", UsersV1 = "UsersV1", PeopleV1 = "PeopleV1", + AssetFacesV1 = "AssetFacesV1", UserMetadataV1 = "UserMetadataV1" } export enum TranscodeHWAccel { diff --git a/server/Dockerfile b/server/Dockerfile index 1f5438d070..e082d0e69e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,26 +1,27 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:202507091427@sha256:733e510024e03bc2450608a1f3622f45bf287b70d89a06e60acbccea4bf4fd8c AS dev +FROM ghcr.io/immich-app/base-server-dev:202507162011@sha256:85d4230c2208646bd6c528db41b2213d780b11b7a311397ca6a2aaba7cf697c8 AS dev WORKDIR /usr/src/app -COPY server/package.json server/package-lock.json ./ +COPY ./server/package* ./server/ +WORKDIR /usr/src/app/server RUN npm ci && \ - # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need - # they're marked as optional dependencies, so we need to copy them manually after pruning - rm -rf node_modules/@img/sharp-libvips* && \ - rm -rf node_modules/@img/sharp-linuxmusl-x64 -ENV PATH="${PATH}:/usr/src/app/bin" \ - IMMICH_ENV=development \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all -ENTRYPOINT ["tini", "--", "/bin/sh"] + # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need + # they're marked as optional dependencies, so we need to copy them manually after pruning + rm -rf node_modules/@img/sharp-libvips* && \ + rm -rf node_modules/@img/sharp-linuxmusl-x64 +ENV PATH="${PATH}:/usr/src/app/server/bin" \ + IMMICH_ENV=development \ + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] FROM dev AS dev-container-server RUN rm -rf /usr/src/app RUN apt-get update && \ - apt-get install sudo inetutils-ping openjdk-11-jre-headless \ - vim nano \ - -y --no-install-recommends --fix-missing + apt-get install sudo inetutils-ping openjdk-11-jre-headless \ + vim nano \ + -y --no-install-recommends --fix-missing RUN usermod -aG sudo node RUN echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers @@ -38,14 +39,14 @@ FROM dev-container-server AS dev-container-mobile USER root # Enable multiarch for arm64 if necessary RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ - dpkg --add-architecture amd64 && \ - apt-get update && \ - apt-get install -y --no-install-recommends \ - qemu-user-static \ - libc6:amd64 \ - libstdc++6:amd64 \ - libgcc1:amd64; \ - fi + dpkg --add-architecture amd64 && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + qemu-user-static \ + libc6:amd64 \ + libstdc++6:amd64 \ + libgcc1:amd64; \ + fi # Flutter SDK # https://flutter.dev/docs/development/tools/sdk/releases?tab=linux @@ -77,45 +78,42 @@ FROM dev AS prod COPY server . RUN npm run build RUN npm prune --omit=dev --omit=optional -COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img -COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl +COPY --from=dev /usr/src/app/server/node_modules/@img ./node_modules/@img +COPY --from=dev /usr/src/app/server/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web -WORKDIR /usr/src/open-api/typescript-sdk -COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ -RUN npm ci -COPY open-api/typescript-sdk/ ./ -RUN npm run build - WORKDIR /usr/src/app -COPY web/package*.json web/svelte.config.js ./ -RUN npm ci -COPY web ./ -COPY i18n ../i18n -RUN npm run build +COPY ./web ./web/ +COPY ./i18n ./i18n/ +COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/ +WORKDIR /usr/src/app/open-api/typescript-sdk +RUN npm ci && npm run build + +WORKDIR /usr/src/app/web +RUN npm ci && npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:202507091427@sha256:ad10451acd8eda05a006a2586b6b0425cdaaca97c413fb4c1a10896712528bdc +FROM ghcr.io/immich-app/base-server-prod:202507162011@sha256:636f3ddb6106628ef851d51c23f3fa2c6e4829390cc315b27b38c288c82b23a7 WORKDIR /usr/src/app ENV NODE_ENV=production \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all -COPY --from=prod /usr/src/app/node_modules ./node_modules -COPY --from=prod /usr/src/app/dist ./dist -COPY --from=prod /usr/src/app/bin ./bin -COPY --from=web /usr/src/app/build /build/www -COPY server/resources resources -COPY server/package.json server/package-lock.json ./ -COPY server/start*.sh ./ -COPY "docker/scripts/get-cpus.sh" ./ -RUN npm install -g @immich/cli && npm cache clean --force + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all + +COPY --from=prod /usr/src/app/server/node_modules ./server/node_modules +COPY --from=prod /usr/src/app/server/dist ./server/dist +COPY --from=prod /usr/src/app/server/bin ./server/bin +COPY --from=web /usr/src/app/web/build /build/www +COPY ./server/resources ./server/resources +COPY ./server/package.json server/package-lock.json ./ COPY LICENSE /licenses/LICENSE.txt COPY LICENSE /LICENSE -ENV PATH="${PATH}:/usr/src/app/bin" + +RUN npm install -g @immich/cli && npm cache clean --force +ENV PATH="${PATH}:/usr/src/app/server/bin" ARG BUILD_ID ARG BUILD_IMAGE @@ -134,7 +132,7 @@ ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE VOLUME /usr/src/app/upload EXPOSE 2283 -ENTRYPOINT ["tini", "--", "/bin/bash"] +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] CMD ["start.sh"] HEALTHCHECK CMD immich-healthcheck diff --git a/docker/scripts/get-cpus.sh b/server/bin/get-cpus.sh similarity index 100% rename from docker/scripts/get-cpus.sh rename to server/bin/get-cpus.sh diff --git a/server/bin/immich-admin b/server/bin/immich-admin index 30fd33a20a..0465a362b8 100755 --- a/server/bin/immich-admin +++ b/server/bin/immich-admin @@ -1,3 +1,3 @@ #!/usr/bin/env sh -/usr/src/app/start.sh immich-admin "$@" +start.sh immich-admin "$@" diff --git a/server/bin/immich-dev b/server/bin/immich-dev index 177455d037..533c10ef9d 100755 --- a/server/bin/immich-dev +++ b/server/bin/immich-dev @@ -1,3 +1,9 @@ #!/usr/bin/env bash -node /usr/src/app/node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch -- "$@" +if [ "$IMMICH_ENV" != "development" ]; then + echo "This command can only be run in development environments" + exit 1 +fi + +cd /usr/src/app/server || exit 1 +npm exec nest start --debug "0.0.0.0:9230" --watch -- "$@" diff --git a/server/bin/start.sh b/server/bin/start.sh new file mode 100755 index 0000000000..2b4351a6bc --- /dev/null +++ b/server/bin/start.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +echo "Initializing Immich $IMMICH_SOURCE_REF" + +lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2" +if [ -f "$lib_path" ]; then + export LD_PRELOAD="$lib_path" +else + echo "skipping libmimalloc - path not found $lib_path" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib" +SERVER_HOME=/usr/src/app/server + +read_file_and_export() { + if [ -n "${!1}" ]; then + content="$(cat "${!1}")" + export "$2"="${content}" + unset "$1" + fi +} +read_file_and_export "DB_URL_FILE" "DB_URL" +read_file_and_export "DB_HOSTNAME_FILE" "DB_HOSTNAME" +read_file_and_export "DB_DATABASE_NAME_FILE" "DB_DATABASE_NAME" +read_file_and_export "DB_USERNAME_FILE" "DB_USERNAME" +read_file_and_export "DB_PASSWORD_FILE" "DB_PASSWORD" +read_file_and_export "REDIS_PASSWORD_FILE" "REDIS_PASSWORD" + +if CPU_CORES="${CPU_CORES:=$(get-cpus.sh 2>/dev/null)}"; then + echo "Detected CPU Cores: $CPU_CORES" + if [ "$CPU_CORES" -gt 4 ]; then + export UV_THREADPOOL_SIZE=$CPU_CORES + fi +else + echo "skipping get-cpus.sh - not found in PATH or failed: using default UV_THREADPOOL_SIZE" +fi + +if [ -f "${SERVER_HOME}/dist/main.js" ]; then + exec node "${SERVER_HOME}/dist/main.js" "$@" +else + echo "Error: ${SERVER_HOME}/dist/main.js not found" + if [ "$IMMICH_ENV" = "development" ]; then + echo "You may need to build the server first." + fi + exit 1 +fi diff --git a/server/package-lock.json b/server/package-lock.json index 46dfa8fd37..11be49ecb6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2882,6 +2882,67 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/platform-express/node_modules/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@nestjs/platform-socket.io": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.3.tgz", @@ -13595,9 +13656,9 @@ } }, "node_modules/multer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", - "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index c06087edea..8d261463e7 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,7 +5,7 @@ import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; -import { commands } from 'src/commands'; +import { commandsAndQuestions } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; import { ImmichWorker } from 'src/enum'; @@ -85,19 +85,19 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { @Module({ imports: [...imports, ScheduleModule.forRoot()], controllers: [...controllers], - providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }], + providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.Api }], }) export class ApiModule extends BaseModule {} @Module({ imports: [...imports], - providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry], + providers: [...common, { provide: IWorker, useValue: ImmichWorker.Microservices }, SchedulerRegistry], }) export class MicroservicesModule extends BaseModule {} @Module({ imports: [...imports], - providers: [...common, ...commands, SchedulerRegistry], + providers: [...common, ...commandsAndQuestions, SchedulerRegistry], }) export class ImmichAdminModule implements OnModuleDestroy { constructor(private service: CliService) {} diff --git a/server/src/commands/index.ts b/server/src/commands/index.ts index ce085f6e34..46a8d13e35 100644 --- a/server/src/commands/index.ts +++ b/server/src/commands/index.ts @@ -1,11 +1,16 @@ import { GrantAdminCommand, PromptEmailQuestion, RevokeAdminCommand } from 'src/commands/grant-admin'; import { ListUsersCommand } from 'src/commands/list-users.command'; +import { + ChangeMediaLocationCommand, + PromptConfirmMoveQuestions, + PromptMediaLocationQuestions, +} from 'src/commands/media-location.command'; import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; import { VersionCommand } from 'src/commands/version.command'; -export const commands = [ +export const commandsAndQuestions = [ ResetAdminPasswordCommand, PromptPasswordQuestions, PromptEmailQuestion, @@ -17,4 +22,7 @@ export const commands = [ VersionCommand, GrantAdminCommand, RevokeAdminCommand, + ChangeMediaLocationCommand, + PromptMediaLocationQuestions, + PromptConfirmMoveQuestions, ]; diff --git a/server/src/commands/media-location.command.ts b/server/src/commands/media-location.command.ts new file mode 100644 index 0000000000..0935fe202d --- /dev/null +++ b/server/src/commands/media-location.command.ts @@ -0,0 +1,106 @@ +import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; +import { CliService } from 'src/services/cli.service'; + +@Command({ + name: 'change-media-location', + description: 'Change database file paths to align with a new media location', +}) +export class ChangeMediaLocationCommand extends CommandRunner { + constructor( + private service: CliService, + private inquirer: InquirerService, + ) { + super(); + } + + private async showSamplePaths(hint?: string) { + hint = hint ? ` (${hint})` : ''; + + const paths = await this.service.getSampleFilePaths(); + if (paths.length > 0) { + let message = ` Examples from the database${hint}:\n`; + for (const path of paths) { + message += ` - ${path}\n`; + } + + console.log(`\n${message}`); + } + } + + async run(): Promise { + try { + await this.showSamplePaths(); + + const { oldValue, newValue } = await this.inquirer.ask<{ oldValue: string; newValue: string }>( + 'prompt-media-location', + {}, + ); + + const success = await this.service.migrateFilePaths({ + oldValue, + newValue, + confirm: async ({ sourceFolder, targetFolder }) => { + console.log(` + Previous value: ${oldValue} + Current value: ${newValue} + + Changing from "${sourceFolder}/*" to "${targetFolder}/*" +`); + + const { value: confirmed } = await this.inquirer.ask<{ value: boolean }>('prompt-confirm-move', {}); + return confirmed; + }, + }); + + const successMessage = `Matching database file paths were updated successfully! 🎉 + + You may now set IMMICH_MEDIA_LOCATION=${newValue} and restart! + + (please remember to update applicable volume mounts e.g + services: + immich-server: + ... + volumes: + - \${UPLOAD_LOCATION}:/usr/src/app/upload + ... + )`; + + console.log(`\n ${success ? successMessage : 'No rows were updated'}\n`); + + await this.showSamplePaths('after'); + } catch (error) { + console.error(error); + console.error('Unable to update database file paths.'); + } + } +} + +const currentValue = process.env.IMMICH_MEDIA_LOCATION || ''; + +const makePrompt = (which: string) => { + return `Enter the ${which} value of IMMICH_MEDIA_LOCATION:${currentValue ? ` [${currentValue}]` : ''}`; +}; + +@QuestionSet({ name: 'prompt-media-location' }) +export class PromptMediaLocationQuestions { + @Question({ message: makePrompt('previous'), name: 'oldValue' }) + oldValue(value: string) { + return value || currentValue; + } + + @Question({ message: makePrompt('new'), name: 'newValue' }) + newValue(value: string) { + return value || currentValue; + } +} + +@QuestionSet({ name: 'prompt-confirm-move' }) +export class PromptConfirmMoveQuestions { + @Question({ + message: 'Do you want to proceed? [Y/n]', + name: 'value', + }) + value(value: string): boolean { + return ['yes', 'y'].includes((value || 'y').toLowerCase()); + } +} diff --git a/server/src/config.ts b/server/src/config.ts index 90ca2c1529..33a6f19ba1 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -8,7 +8,7 @@ import { OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, VideoContainer, @@ -42,7 +42,7 @@ export interface SystemConfig { twoPass: boolean; preferredHwDevice: string; transcode: TranscodePolicy; - accel: TranscodeHWAccel; + accel: TranscodeHardwareAcceleration; accelDecode: boolean; tonemap: ToneMapping; }; @@ -190,39 +190,39 @@ export const defaults = Object.freeze({ preset: 'ultrafast', targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], - targetAudioCodec: AudioCodec.AAC, - acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE], - acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM], + targetAudioCodec: AudioCodec.Aac, + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le], + acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], targetResolution: '720', maxBitrate: '0', bframes: -1, refs: 0, gopSize: 0, temporalAQ: false, - cqMode: CQMode.AUTO, + cqMode: CQMode.Auto, twoPass: false, preferredHwDevice: 'auto', - transcode: TranscodePolicy.REQUIRED, - tonemap: ToneMapping.HABLE, - accel: TranscodeHWAccel.DISABLED, + transcode: TranscodePolicy.Required, + tonemap: ToneMapping.Hable, + accel: TranscodeHardwareAcceleration.Disabled, accelDecode: false, }, job: { - [QueueName.BACKGROUND_TASK]: { concurrency: 5 }, - [QueueName.SMART_SEARCH]: { concurrency: 2 }, - [QueueName.METADATA_EXTRACTION]: { concurrency: 5 }, - [QueueName.FACE_DETECTION]: { concurrency: 2 }, - [QueueName.SEARCH]: { concurrency: 5 }, - [QueueName.SIDECAR]: { concurrency: 5 }, - [QueueName.LIBRARY]: { concurrency: 5 }, - [QueueName.MIGRATION]: { concurrency: 5 }, - [QueueName.THUMBNAIL_GENERATION]: { concurrency: 3 }, - [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, - [QueueName.NOTIFICATION]: { concurrency: 5 }, + [QueueName.BackgroundTask]: { concurrency: 5 }, + [QueueName.SmartSearch]: { concurrency: 2 }, + [QueueName.MetadataExtraction]: { concurrency: 5 }, + [QueueName.FaceDetection]: { concurrency: 2 }, + [QueueName.Search]: { concurrency: 5 }, + [QueueName.Sidecar]: { concurrency: 5 }, + [QueueName.Library]: { concurrency: 5 }, + [QueueName.Migration]: { concurrency: 5 }, + [QueueName.ThumbnailGeneration]: { concurrency: 3 }, + [QueueName.VideoConversion]: { concurrency: 1 }, + [QueueName.Notification]: { concurrency: 5 }, }, logging: { enabled: true, - level: LogLevel.LOG, + level: LogLevel.Log, }, machineLearning: { enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', @@ -273,7 +273,7 @@ export const defaults = Object.freeze({ storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', roleClaim: 'immich_role', - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, timeout: 30_000, }, passwordLogin: { @@ -286,12 +286,12 @@ export const defaults = Object.freeze({ }, image: { thumbnail: { - format: ImageFormat.WEBP, + format: ImageFormat.Webp, size: 250, quality: 80, }, preview: { - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, size: 1440, quality: 80, }, @@ -299,7 +299,7 @@ export const defaults = Object.freeze({ extractEmbedded: false, fullsize: { enabled: false, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, }, }, diff --git a/server/src/constants.ts b/server/src/constants.ts index 2e25797938..2d803c2e95 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -25,14 +25,14 @@ export const EXTENSION_NAMES: Record = { } as const; export const VECTOR_EXTENSIONS = [ - DatabaseExtension.VECTORCHORD, - DatabaseExtension.VECTORS, - DatabaseExtension.VECTOR, + DatabaseExtension.VectorChord, + DatabaseExtension.Vectors, + DatabaseExtension.Vector, ] as const; export const VECTOR_INDEX_TABLES = { - [VectorIndex.CLIP]: 'smart_search', - [VectorIndex.FACE]: 'face_search', + [VectorIndex.Clip]: 'smart_search', + [VectorIndex.Face]: 'face_search', } as const; export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2; @@ -47,7 +47,7 @@ export const serverVersion = new SemVer(version); export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); -export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; +export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || '/usr/src/app/upload'; export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000); export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number( diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index b91f2902d5..d2d34da102 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -20,13 +20,13 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - @Authenticated({ permission: Permission.ACTIVITY_READ }) + @Authenticated({ permission: Permission.ActivityRead }) getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } @Post() - @Authenticated({ permission: Permission.ACTIVITY_CREATE }) + @Authenticated({ permission: Permission.ActivityCreate }) async createActivity( @Auth() auth: AuthDto, @Body() dto: ActivityCreateDto, @@ -40,14 +40,14 @@ export class ActivityController { } @Get('statistics') - @Authenticated({ permission: Permission.ACTIVITY_STATISTICS }) + @Authenticated({ permission: Permission.ActivityStatistics }) getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { return this.service.getStatistics(auth, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.ACTIVITY_DELETE }) + @Authenticated({ permission: Permission.ActivityDelete }) deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index e21d28af0c..865a63e408 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -23,7 +23,7 @@ export class AlbumController { constructor(private service: AlbumService) {} @Get() - @Authenticated({ permission: Permission.ALBUM_READ }) + @Authenticated({ permission: Permission.AlbumRead }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @@ -35,18 +35,18 @@ export class AlbumController { } @Post() - @Authenticated({ permission: Permission.ALBUM_CREATE }) + @Authenticated({ permission: Permission.AlbumCreate }) createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } @Get('statistics') - @Authenticated({ permission: Permission.ALBUM_STATISTICS }) + @Authenticated({ permission: Permission.AlbumStatistics }) getAlbumStatistics(@Auth() auth: AuthDto): Promise { return this.service.getStatistics(auth); } - @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @@ -57,7 +57,7 @@ export class AlbumController { } @Patch(':id') - @Authenticated({ permission: Permission.ALBUM_UPDATE }) + @Authenticated({ permission: Permission.AlbumUpdate }) updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -67,7 +67,7 @@ export class AlbumController { } @Delete(':id') - @Authenticated({ permission: Permission.ALBUM_DELETE }) + @Authenticated({ permission: Permission.AlbumDelete }) deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { return this.service.delete(auth, id); } diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index f58a53723a..993ad012cc 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -55,7 +55,7 @@ describe(APIKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/api-keys/123`) - .send({ name: 'new name', permissions: [Permission.ALL] }); + .send({ name: 'new name', permissions: [Permission.All] }); expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); }); diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 08efd753cf..6347a1274a 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -13,25 +13,25 @@ export class APIKeyController { constructor(private service: ApiKeyService) {} @Post() - @Authenticated({ permission: Permission.API_KEY_CREATE }) + @Authenticated({ permission: Permission.ApiKeyCreate }) createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.API_KEY_READ }) + @Authenticated({ permission: Permission.ApiKeyRead }) getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') - @Authenticated({ permission: Permission.API_KEY_READ }) + @Authenticated({ permission: Permission.ApiKeyRead }) getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.API_KEY_UPDATE }) + @Authenticated({ permission: Permission.ApiKeyUpdate }) updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -42,7 +42,7 @@ export class APIKeyController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.API_KEY_DELETE }) + @Authenticated({ permission: Permission.ApiKeyDelete }) deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index b2c9397580..ea6c9602c8 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -45,7 +45,7 @@ import { ImmichFileResponse, sendFile } from 'src/utils/file'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(RouteKey.ASSET) +@Controller(RouteKey.Asset) export class AssetMediaController { constructor( private logger: LoggingRepository, @@ -56,7 +56,7 @@ export class AssetMediaController { @UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor) @ApiConsumes('multipart/form-data') @ApiHeader({ - name: ImmichHeader.CHECKSUM, + name: ImmichHeader.Checksum, description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded', required: false, }) diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 925b64c8a8..bb17daddf3 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -19,7 +19,7 @@ import { AssetService } from 'src/services/asset.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(RouteKey.ASSET) +@Controller(RouteKey.Asset) export class AssetController { constructor(private service: AssetService) {} diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 78c611d761..9bc5fd0fbb 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -36,9 +36,9 @@ export class AuthController { return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, - { key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, - { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + { key: ImmichCookie.AccessToken, value: body.accessToken }, + { key: ImmichCookie.AuthType, value: AuthType.Password }, + { key: ImmichCookie.IsAuthenticated, value: 'true' }, ], }); } @@ -70,13 +70,13 @@ export class AuthController { @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise { - const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; + const authType = (request.cookies || {})[ImmichCookie.AuthType]; const body = await this.service.logout(auth, authType); return respondWithoutCookie(res, body, [ - ImmichCookie.ACCESS_TOKEN, - ImmichCookie.AUTH_TYPE, - ImmichCookie.IS_AUTHENTICATED, + ImmichCookie.AccessToken, + ImmichCookie.AuthType, + ImmichCookie.IsAuthenticated, ]); } diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index d94cd532f7..20b6db6039 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -19,19 +19,19 @@ export class FaceController { constructor(private service: PersonService) {} @Post() - @Authenticated({ permission: Permission.FACE_CREATE }) + @Authenticated({ permission: Permission.FaceCreate }) createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) { return this.service.createFace(auth, dto); } @Get() - @Authenticated({ permission: Permission.FACE_READ }) + @Authenticated({ permission: Permission.FaceRead }) getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { return this.service.getFacesById(auth, dto); } @Put(':id') - @Authenticated({ permission: Permission.FACE_UPDATE }) + @Authenticated({ permission: Permission.FaceUpdate }) reassignFacesById( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -41,7 +41,7 @@ export class FaceController { } @Delete(':id') - @Authenticated({ permission: Permission.FACE_DELETE }) + @Authenticated({ permission: Permission.FaceDelete }) deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto) { return this.service.deleteFace(auth, id, dto); } diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index b8959ca288..e090586f57 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -19,32 +19,32 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) + @Authenticated({ permission: Permission.LibraryRead, admin: true }) getAllLibraries(): Promise { return this.service.getAll(); } @Post() - @Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true }) + @Authenticated({ permission: Permission.LibraryCreate, admin: true }) createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } @Get(':id') - @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) + @Authenticated({ permission: Permission.LibraryRead, admin: true }) getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @Put(':id') - @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + @Authenticated({ permission: Permission.LibraryUpdate, admin: true }) updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { return this.service.update(id, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) + @Authenticated({ permission: Permission.LibraryDelete, admin: true }) deleteLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.delete(id); } @@ -58,14 +58,14 @@ export class LibraryController { } @Get(':id/statistics') - @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) + @Authenticated({ permission: Permission.LibraryStatistics, admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } @Post(':id/scan') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + @Authenticated({ permission: Permission.LibraryUpdate, admin: true }) scanLibrary(@Param() { id }: UUIDParamDto) { return this.service.queueScan(id); } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index d33c5ec22c..a5bbbd7411 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -20,31 +20,31 @@ export class MemoryController { constructor(private service: MemoryService) {} @Get() - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryRead }) searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ permission: Permission.MEMORY_CREATE }) + @Authenticated({ permission: Permission.MemoryCreate }) createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { return this.service.create(auth, dto); } @Get('statistics') - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryRead }) memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.statistics(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.MEMORY_READ }) + @Authenticated({ permission: Permission.MemoryRead }) getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.MEMORY_UPDATE }) + @Authenticated({ permission: Permission.MemoryUpdate }) updateMemory( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -55,7 +55,7 @@ export class MemoryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.MEMORY_DELETE }) + @Authenticated({ permission: Permission.MemoryDelete }) deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index c64f786850..af4eb198b6 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -19,31 +19,31 @@ export class NotificationController { constructor(private service: NotificationService) {} @Get() - @Authenticated({ permission: Permission.NOTIFICATION_READ }) + @Authenticated({ permission: Permission.NotificationRead }) getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise { return this.service.search(auth, dto); } @Put() - @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + @Authenticated({ permission: Permission.NotificationUpdate }) updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise { return this.service.updateAll(auth, dto); } @Delete() - @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + @Authenticated({ permission: Permission.NotificationDelete }) deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.NOTIFICATION_READ }) + @Authenticated({ permission: Permission.NotificationRead }) getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + @Authenticated({ permission: Permission.NotificationUpdate }) updateNotification( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -53,7 +53,7 @@ export class NotificationController { } @Delete(':id') - @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + @Authenticated({ permission: Permission.NotificationDelete }) deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 23ddff5ddc..7da75f573a 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -41,8 +41,8 @@ export class OAuthController { { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.OAUTH_STATE, value: state }, - { key: ImmichCookie.OAUTH_CODE_VERIFIER, value: codeVerifier }, + { key: ImmichCookie.OAuthState, value: state }, + { key: ImmichCookie.OAuthCodeVerifier, value: codeVerifier }, ], }, ); @@ -56,14 +56,14 @@ export class OAuthController { @GetLoginDetails() loginDetails: LoginDetails, ): Promise { const body = await this.service.callback(dto, request.headers, loginDetails); - res.clearCookie(ImmichCookie.OAUTH_STATE); - res.clearCookie(ImmichCookie.OAUTH_CODE_VERIFIER); + res.clearCookie(ImmichCookie.OAuthState); + res.clearCookie(ImmichCookie.OAuthCodeVerifier); return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, values: [ - { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, - { key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, - { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + { key: ImmichCookie.AccessToken, value: body.accessToken }, + { key: ImmichCookie.AuthType, value: AuthType.OAuth }, + { key: ImmichCookie.IsAuthenticated, value: 'true' }, ], }); } diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index 6830fdd52f..6b6efaa570 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -13,19 +13,19 @@ export class PartnerController { constructor(private service: PartnerService) {} @Get() - @Authenticated({ permission: Permission.PARTNER_READ }) + @Authenticated({ permission: Permission.PartnerRead }) getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise { return this.service.search(auth, dto); } @Post(':id') - @Authenticated({ permission: Permission.PARTNER_CREATE }) + @Authenticated({ permission: Permission.PartnerCreate }) createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.PARTNER_UPDATE }) + @Authenticated({ permission: Permission.PartnerUpdate }) updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -35,7 +35,7 @@ export class PartnerController { } @Delete(':id') - @Authenticated({ permission: Permission.PARTNER_DELETE }) + @Authenticated({ permission: Permission.PartnerDelete }) removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index a9f6616426..ec66f7a9ca 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -45,38 +45,38 @@ export class PersonController { } @Get() - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise { return this.service.getAll(auth, options); } @Post() - @Authenticated({ permission: Permission.PERSON_CREATE }) + @Authenticated({ permission: Permission.PersonCreate }) createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { return this.service.create(auth, dto); } @Put() - @Authenticated({ permission: Permission.PERSON_UPDATE }) + @Authenticated({ permission: Permission.PersonUpdate }) updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.PERSON_DELETE }) + @Authenticated({ permission: Permission.PersonDelete }) deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.PERSON_UPDATE }) + @Authenticated({ permission: Permission.PersonUpdate }) updatePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -87,20 +87,20 @@ export class PersonController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.PERSON_DELETE }) + @Authenticated({ permission: Permission.PersonDelete }) deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } @Get(':id/statistics') - @Authenticated({ permission: Permission.PERSON_STATISTICS }) + @Authenticated({ permission: Permission.PersonStatistics }) getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @FileResponse() - @Authenticated({ permission: Permission.PERSON_READ }) + @Authenticated({ permission: Permission.PersonRead }) async getPersonThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -111,7 +111,7 @@ export class PersonController { } @Put(':id/reassign') - @Authenticated({ permission: Permission.PERSON_REASSIGN }) + @Authenticated({ permission: Permission.PersonReassign }) reassignFaces( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -121,7 +121,7 @@ export class PersonController { } @Post(':id/merge') - @Authenticated({ permission: Permission.PERSON_MERGE }) + @Authenticated({ permission: Permission.PersonMerge }) mergePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index f5eb10b3dd..cbe8158fee 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -13,26 +13,26 @@ export class SessionController { constructor(private service: SessionService) {} @Post() - @Authenticated({ permission: Permission.SESSION_CREATE }) + @Authenticated({ permission: Permission.SessionCreate }) createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.SESSION_READ }) + @Authenticated({ permission: Permission.SessionRead }) getSessions(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Delete() - @Authenticated({ permission: Permission.SESSION_DELETE }) + @Authenticated({ permission: Permission.SessionDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteAllSessions(@Auth() auth: AuthDto): Promise { return this.service.deleteAll(auth); } @Put(':id') - @Authenticated({ permission: Permission.SESSION_UPDATE }) + @Authenticated({ permission: Permission.SessionUpdate }) updateSession( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -42,14 +42,14 @@ export class SessionController { } @Delete(':id') - @Authenticated({ permission: Permission.SESSION_DELETE }) + @Authenticated({ permission: Permission.SessionDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } @Post(':id/lock') - @Authenticated({ permission: Permission.SESSION_LOCK }) + @Authenticated({ permission: Permission.SessionLock }) @HttpCode(HttpStatus.NO_CONTENT) lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.lock(auth, id); diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index ca978f03da..273d625ca7 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -24,7 +24,7 @@ export class SharedLinkController { constructor(private service: SharedLinkService) {} @Get() - @Authenticated({ permission: Permission.SHARED_LINK_READ }) + @Authenticated({ permission: Permission.SharedLinkRead }) getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise { return this.service.getAll(auth, dto); } @@ -38,31 +38,31 @@ export class SharedLinkController { @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; + const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken]; if (sharedLinkToken) { dto.token = sharedLinkToken; } const body = await this.service.getMine(auth, dto); return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, - values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], + values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [], }); } @Get(':id') - @Authenticated({ permission: Permission.SHARED_LINK_READ }) + @Authenticated({ permission: Permission.SharedLinkRead }) getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Post() - @Authenticated({ permission: Permission.SHARED_LINK_CREATE }) + @Authenticated({ permission: Permission.SharedLinkCreate }) createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { return this.service.create(auth, dto); } @Patch(':id') - @Authenticated({ permission: Permission.SHARED_LINK_UPDATE }) + @Authenticated({ permission: Permission.SharedLinkUpdate }) updateSharedLink( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -72,7 +72,7 @@ export class SharedLinkController { } @Delete(':id') - @Authenticated({ permission: Permission.SHARED_LINK_DELETE }) + @Authenticated({ permission: Permission.SharedLinkDelete }) removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/stack.controller.ts b/server/src/controllers/stack.controller.ts index 188952eba5..238753734c 100644 --- a/server/src/controllers/stack.controller.ts +++ b/server/src/controllers/stack.controller.ts @@ -14,32 +14,32 @@ export class StackController { constructor(private service: StackService) {} @Get() - @Authenticated({ permission: Permission.STACK_READ }) + @Authenticated({ permission: Permission.StackRead }) searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise { return this.service.search(auth, query); } @Post() - @Authenticated({ permission: Permission.STACK_CREATE }) + @Authenticated({ permission: Permission.StackCreate }) createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise { return this.service.create(auth, dto); } @Delete() - @Authenticated({ permission: Permission.STACK_DELETE }) + @Authenticated({ permission: Permission.StackDelete }) @HttpCode(HttpStatus.NO_CONTENT) deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.deleteAll(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.STACK_READ }) + @Authenticated({ permission: Permission.StackRead }) getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.STACK_UPDATE }) + @Authenticated({ permission: Permission.StackUpdate }) updateStack( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -50,7 +50,7 @@ export class StackController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.STACK_DELETE }) + @Authenticated({ permission: Permission.StackDelete }) deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 58e8bde87b..69117f4d45 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -15,25 +15,25 @@ export class SystemConfigController { ) {} @Get() - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getConfig(): Promise { return this.service.getSystemConfig(); } @Get('defaults') - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getConfigDefaults(): SystemConfigDto { return this.service.getDefaults(); } @Put() - @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) + @Authenticated({ permission: Permission.SystemConfigUpdate, admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { return this.service.updateSystemConfig(dto); } @Get('storage-template-options') - @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) + @Authenticated({ permission: Permission.SystemConfigRead, admin: true }) getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.storageTemplateService.getStorageTemplateOptions(); } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index 71c37d02c4..ad2245a391 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -15,26 +15,26 @@ export class SystemMetadataController { constructor(private service: SystemMetadataService) {} @Get('admin-onboarding') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getAdminOnboarding(): Promise { return this.service.getAdminOnboarding(); } @Post('admin-onboarding') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true }) updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { return this.service.updateAdminOnboarding(dto); } @Get('reverse-geocoding-state') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } @Get('version-check-state') - @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + @Authenticated({ permission: Permission.SystemMetadataRead, admin: true }) getVersionCheckState(): Promise { return this.service.getVersionCheckState(); } diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index cf6b8ac695..4906bc0c6e 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -21,50 +21,50 @@ export class TagController { constructor(private service: TagService) {} @Post() - @Authenticated({ permission: Permission.TAG_CREATE }) + @Authenticated({ permission: Permission.TagCreate }) createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated({ permission: Permission.TAG_READ }) + @Authenticated({ permission: Permission.TagRead }) getAllTags(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Put() - @Authenticated({ permission: Permission.TAG_CREATE }) + @Authenticated({ permission: Permission.TagCreate }) upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise { return this.service.upsert(auth, dto); } @Put('assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise { return this.service.bulkTagAssets(auth, dto); } @Get(':id') - @Authenticated({ permission: Permission.TAG_READ }) + @Authenticated({ permission: Permission.TagRead }) getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.TAG_UPDATE }) + @Authenticated({ permission: Permission.TagUpdate }) updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.TAG_DELETE }) + @Authenticated({ permission: Permission.TagDelete }) deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } @Put(':id/assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) tagAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -74,7 +74,7 @@ export class TagController { } @Delete(':id/assets') - @Authenticated({ permission: Permission.TAG_ASSET }) + @Authenticated({ permission: Permission.TagAsset }) untagAssets( @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index b4ee042625..8cab840ec8 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -12,13 +12,13 @@ export class TimelineController { constructor(private service: TimelineService) {} @Get('buckets') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) { return this.service.getTimeBuckets(auth, dto); } @Get('bucket') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @ApiOkResponse({ type: TimeBucketAssetResponseDto }) @Header('Content-Type', 'application/json') getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) { diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index dfcdfa6ba2..1bb46e4f98 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -14,21 +14,21 @@ export class TrashController { @Post('empty') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) emptyTrash(@Auth() auth: AuthDto): Promise { return this.service.empty(auth); } @Post('restore') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) restoreTrash(@Auth() auth: AuthDto): Promise { return this.service.restore(auth); } @Post('restore/assets') @HttpCode(HttpStatus.OK) - @Authenticated({ permission: Permission.ASSET_DELETE }) + @Authenticated({ permission: Permission.AssetDelete }) restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.restoreAssets(auth, dto); } diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index 83d7caef08..d50bd174ad 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -21,25 +21,25 @@ export class UserAdminController { constructor(private service: UserAdminService) {} @Get() - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserCreate, admin: true }) createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise { return this.service.create(createUserDto); } @Get(':id') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) updateUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -49,7 +49,7 @@ export class UserAdminController { } @Delete(':id') - @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) + @Authenticated({ permission: Permission.AdminUserDelete, admin: true }) deleteUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -59,7 +59,7 @@ export class UserAdminController { } @Get(':id/statistics') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserStatisticsAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -69,13 +69,13 @@ export class UserAdminController { } @Get(':id/preferences') - @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + @Authenticated({ permission: Permission.AdminUserRead, admin: true }) getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getPreferences(auth, id); } @Put(':id/preferences') - @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) + @Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) updateUserPreferencesAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -85,7 +85,7 @@ export class UserAdminController { } @Post(':id/restore') - @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) + @Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @HttpCode(HttpStatus.OK) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 6c6eae15ff..76d2cf4c5a 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -30,7 +30,7 @@ import { sendFile } from 'src/utils/file'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Users') -@Controller(RouteKey.USER) +@Controller(RouteKey.User) export class UserController { constructor( private service: UserService, diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 1a8e31e86b..6576b397e3 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -25,8 +25,8 @@ export interface MoveRequest { }; } -export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL | AssetPathType.FULLSIZE; -export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; +export type GeneratedImageType = AssetPathType.Preview | AssetPathType.Thumbnail | AssetPathType.FullSize; +export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo; export type ThumbnailPathEntity = { id: string; ownerId: string }; @@ -79,7 +79,7 @@ export class StorageCore { } static getLibraryFolder(user: { storageLabel: string | null; id: string }) { - return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id); + return join(StorageCore.getBaseFolder(StorageFolder.Library), user.storageLabel || user.id); } static getBaseFolder(folder: StorageFolder) { @@ -87,23 +87,23 @@ export class StorageCore { } static getPersonThumbnailPath(person: ThumbnailPathEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); + return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); } static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); + return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`); } static getEncodedVideoPath(asset: ThumbnailPathEntity) { - return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`); + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`); } static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) { - return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${uuid}-MP.mp4`); + return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`); } static isAndroidMotionPath(originalPath: string) { - return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO)); + return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.EncodedVideo)); } static isImmichPath(path: string) { @@ -130,7 +130,7 @@ export class StorageCore { async moveAssetVideo(asset: StorageAsset) { return this.moveFile({ entityId: asset.id, - pathType: AssetPathType.ENCODED_VIDEO, + pathType: AssetPathType.EncodedVideo, oldPath: asset.encodedVideoPath, newPath: StorageCore.getEncodedVideoPath(asset), }); @@ -139,7 +139,7 @@ export class StorageCore { async movePersonFile(person: { id: string; ownerId: string; thumbnailPath: string }, pathType: PersonPathType) { const { id: entityId, thumbnailPath } = person; switch (pathType) { - case PersonPathType.FACE: { + case PersonPathType.Face: { await this.moveFile({ entityId, pathType, @@ -188,7 +188,7 @@ export class StorageCore { move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath }); } - if (pathType === AssetPathType.ORIGINAL && !assetInfo) { + if (pathType === AssetPathType.Original && !assetInfo) { this.logger.warn(`Unable to complete move. Missing asset info for ${entityId}`); return; } @@ -274,25 +274,25 @@ export class StorageCore { private savePath(pathType: PathType, id: string, newPath: string) { switch (pathType) { - case AssetPathType.ORIGINAL: { + case AssetPathType.Original: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.FULLSIZE: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FULLSIZE, path: newPath }); + case AssetPathType.FullSize: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath }); } - case AssetPathType.PREVIEW: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); + case AssetPathType.Preview: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath }); } - case AssetPathType.THUMBNAIL: { - return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); + case AssetPathType.Thumbnail: { + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath }); } - case AssetPathType.ENCODED_VIDEO: { + case AssetPathType.EncodedVideo: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); } - case AssetPathType.SIDECAR: { + case AssetPathType.Sidecar: { return this.assetRepository.update({ id, sidecarPath: newPath }); } - case PersonPathType.FACE: { + case PersonPathType.Face: { return this.personRepository.update({ id, thumbnailPath: newPath }); } } diff --git a/server/src/database.ts b/server/src/database.ts index d42b2618a4..dc99fc5b31 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -272,6 +272,8 @@ export type AssetFace = { personId: string | null; sourceType: SourceType; person?: Person | null; + updatedAt: Date; + updateId: string; }; const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const; diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 766e7c70b9..b88f2d2d7e 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -131,7 +131,7 @@ export interface GenerateSqlQueries { } export const Telemetry = (options: { enabled?: boolean }) => - SetMetadata(MetadataKey.TELEMETRY_ENABLED, options?.enabled ?? true); + SetMetadata(MetadataKey.TelemetryEnabled, options?.enabled ?? true); /** Decorator to enable versioning/tracking of generated Sql */ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); @@ -145,13 +145,13 @@ export type EventConfig = { /** register events for these workers, defaults to all workers */ workers?: ImmichWorker[]; }; -export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EVENT_CONFIG, config); +export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EventConfig, config); export type JobConfig = { name: JobName; queue: QueueName; }; -export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JOB_CONFIG, config); +export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JobConfig, config); type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleMetadata = { diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index c6cde0894f..3a88ba5be3 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -18,7 +18,7 @@ export class AlbumUserAddDto { @ValidateUUID() userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.Editor }) role?: AlbumUserRole; } diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 5b587e59ba..98ed8669f0 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -205,7 +205,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset localDateTime: entity.localDateTime, updatedAt: entity.updatedAt, isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false, - isArchived: entity.visibility === AssetVisibility.ARCHIVE, + isArchived: entity.visibility === AssetVisibility.Archive, isTrashed: !!entity.deletedAt, visibility: entity.visibility, duration: entity.duration ?? '0:00:00.00000', diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 727ab1625d..5728d21646 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -126,8 +126,8 @@ export class AssetStatsResponseDto { export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { - images: stats[AssetType.IMAGE], - videos: stats[AssetType.VIDEO], + images: stats[AssetType.Image], + videos: stats[AssetType.Video], total: Object.values(stats).reduce((total, value) => total + value, 0), }; }; diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index e94818b2b5..2bb98b34a5 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -45,7 +45,7 @@ export class LoginResponseDto { export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { const onboardingMetadata = entity.metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + (item): item is UserMetadataItem => item.key === UserMetadataKey.Onboarding, )?.value; return { diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 99fd1d2149..3543d8dae9 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,5 +1,5 @@ import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString } from 'class-validator'; +import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; import { DatabaseSslMode, ImmichEnvironment, LogLevel } from 'src/enum'; import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; @@ -48,6 +48,10 @@ export class EnvDto { @Optional() IMMICH_LOG_LEVEL?: LogLevel; + @Optional() + @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) + IMMICH_MEDIA_LOCATION?: string; + @IsInt() @Optional() @Type(() => Number) diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index 60124c877a..2123b65878 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -50,47 +50,47 @@ export class JobStatusDto { export class AllJobStatusResponseDto implements Record { @ApiProperty({ type: JobStatusDto }) - [QueueName.THUMBNAIL_GENERATION]!: JobStatusDto; + [QueueName.ThumbnailGeneration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.METADATA_EXTRACTION]!: JobStatusDto; + [QueueName.MetadataExtraction]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.VIDEO_CONVERSION]!: JobStatusDto; + [QueueName.VideoConversion]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SMART_SEARCH]!: JobStatusDto; + [QueueName.SmartSearch]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto; + [QueueName.StorageTemplateMigration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.MIGRATION]!: JobStatusDto; + [QueueName.Migration]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.BACKGROUND_TASK]!: JobStatusDto; + [QueueName.BackgroundTask]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SEARCH]!: JobStatusDto; + [QueueName.Search]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.DUPLICATE_DETECTION]!: JobStatusDto; + [QueueName.DuplicateDetection]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.FACE_DETECTION]!: JobStatusDto; + [QueueName.FaceDetection]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.FACIAL_RECOGNITION]!: JobStatusDto; + [QueueName.FacialRecognition]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.SIDECAR]!: JobStatusDto; + [QueueName.Sidecar]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.LIBRARY]!: JobStatusDto; + [QueueName.Library]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.NOTIFICATION]!: JobStatusDto; + [QueueName.Notification]!: JobStatusDto; @ApiProperty({ type: JobStatusDto }) - [QueueName.BACKUP_DATABASE]!: JobStatusDto; + [QueueName.BackupDatabase]!: JobStatusDto; } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index e92e11bdfb..a79511c73e 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -50,7 +50,7 @@ export class MemoryCreateDto extends MemoryBaseDto { @ValidateNested() @Type((options) => { switch (options?.object.type) { - case MemoryType.ON_THIS_DAY: { + case MemoryType.OnThisDay: { return OnThisDayDto; } diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 85c6fbf0de..aef78e51ea 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -170,7 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto { @Optional() encodedVideoPath?: string; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.DESC }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.Desc }) order?: AssetOrder; @IsInt() diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 9725539e3d..e0c9c059c4 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -245,7 +245,6 @@ export class SyncPersonV1 { ownerId!: string; name!: string; birthDate!: Date | null; - thumbnailPath!: string; isHidden!: boolean; isFavorite!: boolean; color!: string | null; @@ -257,6 +256,25 @@ export class SyncPersonDeleteV1 { personId!: string; } +@ExtraModel() +export class SyncAssetFaceV1 { + id!: string; + assetId!: string; + personId!: string | null; + imageWidth!: number; + imageHeight!: number; + boundingBoxX1!: number; + boundingBoxY1!: number; + boundingBoxX2!: number; + boundingBoxY2!: number; + sourceType!: string; +} + +@ExtraModel() +export class SyncAssetFaceDeleteV1 { + assetFaceId!: string; +} + @ExtraModel() export class SyncUserMetadataV1 { userId!: string; @@ -312,6 +330,8 @@ export type SyncItem = { [SyncEntityType.PartnerStackV1]: SyncStackV1; [SyncEntityType.PersonV1]: SyncPersonV1; [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; + [SyncEntityType.AssetFaceV1]: SyncAssetFaceV1; + [SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1; [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; [SyncEntityType.SyncAckV1]: SyncAckV1; diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 809f381dd6..8a58995de7 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -26,7 +26,7 @@ import { OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, VideoContainer, @@ -136,8 +136,8 @@ export class SystemConfigFFmpegDto { @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' }) transcode!: TranscodePolicy; - @ValidateEnum({ enum: TranscodeHWAccel, name: 'TranscodeHWAccel' }) - accel!: TranscodeHWAccel; + @ValidateEnum({ enum: TranscodeHardwareAcceleration, name: 'TranscodeHWAccel' }) + accel!: TranscodeHardwareAcceleration; @ValidateBoolean() accelDecode!: boolean; @@ -158,67 +158,67 @@ class SystemConfigJobDto implements Record @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; + [QueueName.ThumbnailGeneration]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; + [QueueName.MetadataExtraction]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; + [QueueName.VideoConversion]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SMART_SEARCH]!: JobSettingsDto; + [QueueName.SmartSearch]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.MIGRATION]!: JobSettingsDto; + [QueueName.Migration]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.BACKGROUND_TASK]!: JobSettingsDto; + [QueueName.BackgroundTask]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SEARCH]!: JobSettingsDto; + [QueueName.Search]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.FACE_DETECTION]!: JobSettingsDto; + [QueueName.FaceDetection]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.SIDECAR]!: JobSettingsDto; + [QueueName.Sidecar]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.LIBRARY]!: JobSettingsDto; + [QueueName.Library]!: JobSettingsDto; @ApiProperty({ type: JobSettingsDto }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) - [QueueName.NOTIFICATION]!: JobSettingsDto; + [QueueName.Notification]!: JobSettingsDto; } class SystemConfigLibraryScanDto { diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index d165438061..b258158ae2 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -157,7 +157,7 @@ export class UserPreferencesUpdateDto { class AlbumsResponse { @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) - defaultAssetOrder: AssetOrder = AssetOrder.DESC; + defaultAssetOrder: AssetOrder = AssetOrder.Desc; } class RatingsResponse { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 3e3a92d42e..0da86bfcb5 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -171,7 +171,7 @@ export class UserAdminResponseDto extends UserResponseDto { export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; const license = metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.LICENSE, + (item): item is UserMetadataItem => item.key === UserMetadataKey.License, )?.value; return { ...mapUser(entity), diff --git a/server/src/enum.ts b/server/src/enum.ts index d7c74a71c6..f2eae615ab 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,402 +1,398 @@ export enum AuthType { - PASSWORD = 'password', - OAUTH = 'oauth', + Password = 'password', + OAuth = 'oauth', } export enum ImmichCookie { - ACCESS_TOKEN = 'immich_access_token', - AUTH_TYPE = 'immich_auth_type', - IS_AUTHENTICATED = 'immich_is_authenticated', - SHARED_LINK_TOKEN = 'immich_shared_link_token', - OAUTH_STATE = 'immich_oauth_state', - OAUTH_CODE_VERIFIER = 'immich_oauth_code_verifier', + AccessToken = 'immich_access_token', + AuthType = 'immich_auth_type', + IsAuthenticated = 'immich_is_authenticated', + SharedLinkToken = 'immich_shared_link_token', + OAuthState = 'immich_oauth_state', + OAuthCodeVerifier = 'immich_oauth_code_verifier', } export enum ImmichHeader { - API_KEY = 'x-api-key', - USER_TOKEN = 'x-immich-user-token', - SESSION_TOKEN = 'x-immich-session-token', - SHARED_LINK_KEY = 'x-immich-share-key', - CHECKSUM = 'x-immich-checksum', - CID = 'x-immich-cid', + ApiKey = 'x-api-key', + UserToken = 'x-immich-user-token', + SessionToken = 'x-immich-session-token', + SharedLinkKey = 'x-immich-share-key', + Checksum = 'x-immich-checksum', + Cid = 'x-immich-cid', } export enum ImmichQuery { - SHARED_LINK_KEY = 'key', - API_KEY = 'apiKey', - SESSION_KEY = 'sessionKey', + SharedLinkKey = 'key', + ApiKey = 'apiKey', + SessionKey = 'sessionKey', } export enum AssetType { - IMAGE = 'IMAGE', - VIDEO = 'VIDEO', - AUDIO = 'AUDIO', - OTHER = 'OTHER', + Image = 'IMAGE', + Video = 'VIDEO', + Audio = 'AUDIO', + Other = 'OTHER', } export enum AssetFileType { /** * An full/large-size image extracted/converted from RAW photos */ - FULLSIZE = 'fullsize', - PREVIEW = 'preview', - THUMBNAIL = 'thumbnail', + FullSize = 'fullsize', + Preview = 'preview', + Thumbnail = 'thumbnail', } export enum AlbumUserRole { - EDITOR = 'editor', - VIEWER = 'viewer', + Editor = 'editor', + Viewer = 'viewer', } export enum AssetOrder { - ASC = 'asc', - DESC = 'desc', + Asc = 'asc', + Desc = 'desc', } export enum DatabaseAction { - CREATE = 'CREATE', - UPDATE = 'UPDATE', - DELETE = 'DELETE', + Create = 'CREATE', + Update = 'UPDATE', + Delete = 'DELETE', } export enum EntityType { - ASSET = 'ASSET', - ALBUM = 'ALBUM', + Asset = 'ASSET', + Album = 'ALBUM', } export enum MemoryType { /** pictures taken on this day X years ago */ - ON_THIS_DAY = 'on_this_day', + OnThisDay = 'on_this_day', } export enum Permission { - ALL = 'all', + All = 'all', - ACTIVITY_CREATE = 'activity.create', - ACTIVITY_READ = 'activity.read', - ACTIVITY_UPDATE = 'activity.update', - ACTIVITY_DELETE = 'activity.delete', - ACTIVITY_STATISTICS = 'activity.statistics', + ActivityCreate = 'activity.create', + ActivityRead = 'activity.read', + ActivityUpdate = 'activity.update', + ActivityDelete = 'activity.delete', + ActivityStatistics = 'activity.statistics', - API_KEY_CREATE = 'apiKey.create', - API_KEY_READ = 'apiKey.read', - API_KEY_UPDATE = 'apiKey.update', - API_KEY_DELETE = 'apiKey.delete', + ApiKeyCreate = 'apiKey.create', + ApiKeyRead = 'apiKey.read', + ApiKeyUpdate = 'apiKey.update', + ApiKeyDelete = 'apiKey.delete', // ASSET_CREATE = 'asset.create', - ASSET_READ = 'asset.read', - ASSET_UPDATE = 'asset.update', - ASSET_DELETE = 'asset.delete', - ASSET_SHARE = 'asset.share', - ASSET_VIEW = 'asset.view', - ASSET_DOWNLOAD = 'asset.download', - ASSET_UPLOAD = 'asset.upload', + AssetRead = 'asset.read', + AssetUpdate = 'asset.update', + AssetDelete = 'asset.delete', + AssetShare = 'asset.share', + AssetView = 'asset.view', + AssetDownload = 'asset.download', + AssetUpload = 'asset.upload', - ALBUM_CREATE = 'album.create', - ALBUM_READ = 'album.read', - ALBUM_UPDATE = 'album.update', - ALBUM_DELETE = 'album.delete', - ALBUM_STATISTICS = 'album.statistics', + AlbumCreate = 'album.create', + AlbumRead = 'album.read', + AlbumUpdate = 'album.update', + AlbumDelete = 'album.delete', + AlbumStatistics = 'album.statistics', - ALBUM_ADD_ASSET = 'album.addAsset', - ALBUM_REMOVE_ASSET = 'album.removeAsset', - ALBUM_SHARE = 'album.share', - ALBUM_DOWNLOAD = 'album.download', + AlbumAddAsset = 'album.addAsset', + AlbumRemoveAsset = 'album.removeAsset', + AlbumShare = 'album.share', + AlbumDownload = 'album.download', - AUTH_DEVICE_DELETE = 'authDevice.delete', + AuthDeviceDelete = 'authDevice.delete', - ARCHIVE_READ = 'archive.read', + ArchiveRead = 'archive.read', - FACE_CREATE = 'face.create', - FACE_READ = 'face.read', - FACE_UPDATE = 'face.update', - FACE_DELETE = 'face.delete', + FaceCreate = 'face.create', + FaceRead = 'face.read', + FaceUpdate = 'face.update', + FaceDelete = 'face.delete', - LIBRARY_CREATE = 'library.create', - LIBRARY_READ = 'library.read', - LIBRARY_UPDATE = 'library.update', - LIBRARY_DELETE = 'library.delete', - LIBRARY_STATISTICS = 'library.statistics', + LibraryCreate = 'library.create', + LibraryRead = 'library.read', + LibraryUpdate = 'library.update', + LibraryDelete = 'library.delete', + LibraryStatistics = 'library.statistics', - TIMELINE_READ = 'timeline.read', - TIMELINE_DOWNLOAD = 'timeline.download', + TimelineRead = 'timeline.read', + TimelineDownload = 'timeline.download', - MEMORY_CREATE = 'memory.create', - MEMORY_READ = 'memory.read', - MEMORY_UPDATE = 'memory.update', - MEMORY_DELETE = 'memory.delete', + MemoryCreate = 'memory.create', + MemoryRead = 'memory.read', + MemoryUpdate = 'memory.update', + MemoryDelete = 'memory.delete', - NOTIFICATION_CREATE = 'notification.create', - NOTIFICATION_READ = 'notification.read', - NOTIFICATION_UPDATE = 'notification.update', - NOTIFICATION_DELETE = 'notification.delete', + NotificationCreate = 'notification.create', + NotificationRead = 'notification.read', + NotificationUpdate = 'notification.update', + NotificationDelete = 'notification.delete', - PARTNER_CREATE = 'partner.create', - PARTNER_READ = 'partner.read', - PARTNER_UPDATE = 'partner.update', - PARTNER_DELETE = 'partner.delete', + PartnerCreate = 'partner.create', + PartnerRead = 'partner.read', + PartnerUpdate = 'partner.update', + PartnerDelete = 'partner.delete', - PERSON_CREATE = 'person.create', - PERSON_READ = 'person.read', - PERSON_UPDATE = 'person.update', - PERSON_DELETE = 'person.delete', - PERSON_STATISTICS = 'person.statistics', - PERSON_MERGE = 'person.merge', - PERSON_REASSIGN = 'person.reassign', + PersonCreate = 'person.create', + PersonRead = 'person.read', + PersonUpdate = 'person.update', + PersonDelete = 'person.delete', + PersonStatistics = 'person.statistics', + PersonMerge = 'person.merge', + PersonReassign = 'person.reassign', - SESSION_CREATE = 'session.create', - SESSION_READ = 'session.read', - SESSION_UPDATE = 'session.update', - SESSION_DELETE = 'session.delete', - SESSION_LOCK = 'session.lock', + SessionCreate = 'session.create', + SessionRead = 'session.read', + SessionUpdate = 'session.update', + SessionDelete = 'session.delete', + SessionLock = 'session.lock', - SHARED_LINK_CREATE = 'sharedLink.create', - SHARED_LINK_READ = 'sharedLink.read', - SHARED_LINK_UPDATE = 'sharedLink.update', - SHARED_LINK_DELETE = 'sharedLink.delete', + SharedLinkCreate = 'sharedLink.create', + SharedLinkRead = 'sharedLink.read', + SharedLinkUpdate = 'sharedLink.update', + SharedLinkDelete = 'sharedLink.delete', - STACK_CREATE = 'stack.create', - STACK_READ = 'stack.read', - STACK_UPDATE = 'stack.update', - STACK_DELETE = 'stack.delete', + StackCreate = 'stack.create', + StackRead = 'stack.read', + StackUpdate = 'stack.update', + StackDelete = 'stack.delete', - SYSTEM_CONFIG_READ = 'systemConfig.read', - SYSTEM_CONFIG_UPDATE = 'systemConfig.update', + SystemConfigRead = 'systemConfig.read', + SystemConfigUpdate = 'systemConfig.update', - SYSTEM_METADATA_READ = 'systemMetadata.read', - SYSTEM_METADATA_UPDATE = 'systemMetadata.update', + SystemMetadataRead = 'systemMetadata.read', + SystemMetadataUpdate = 'systemMetadata.update', - TAG_CREATE = 'tag.create', - TAG_READ = 'tag.read', - TAG_UPDATE = 'tag.update', - TAG_DELETE = 'tag.delete', - TAG_ASSET = 'tag.asset', + TagCreate = 'tag.create', + TagRead = 'tag.read', + TagUpdate = 'tag.update', + TagDelete = 'tag.delete', + TagAsset = 'tag.asset', - ADMIN_USER_CREATE = 'admin.user.create', - ADMIN_USER_READ = 'admin.user.read', - ADMIN_USER_UPDATE = 'admin.user.update', - ADMIN_USER_DELETE = 'admin.user.delete', + AdminUserCreate = 'admin.user.create', + AdminUserRead = 'admin.user.read', + AdminUserUpdate = 'admin.user.update', + AdminUserDelete = 'admin.user.delete', } export enum SharedLinkType { - ALBUM = 'ALBUM', + Album = 'ALBUM', /** * Individual asset * or group of assets that are not in an album */ - INDIVIDUAL = 'INDIVIDUAL', + Individual = 'INDIVIDUAL', } export enum StorageFolder { - ENCODED_VIDEO = 'encoded-video', - LIBRARY = 'library', - UPLOAD = 'upload', - PROFILE = 'profile', - THUMBNAILS = 'thumbs', - BACKUPS = 'backups', + EncodedVideo = 'encoded-video', + Library = 'library', + Upload = 'upload', + Profile = 'profile', + Thumbnails = 'thumbs', + Backups = 'backups', } export enum SystemMetadataKey { - REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', - FACIAL_RECOGNITION_STATE = 'facial-recognition-state', - MEMORIES_STATE = 'memories-state', - ADMIN_ONBOARDING = 'admin-onboarding', - SYSTEM_CONFIG = 'system-config', - SYSTEM_FLAGS = 'system-flags', - VERSION_CHECK_STATE = 'version-check-state', - LICENSE = 'license', + MediaLocation = 'MediaLocation', + ReverseGeocodingState = 'reverse-geocoding-state', + FacialRecognitionState = 'facial-recognition-state', + MemoriesState = 'memories-state', + AdminOnboarding = 'admin-onboarding', + SystemConfig = 'system-config', + SystemFlags = 'system-flags', + VersionCheckState = 'version-check-state', + License = 'license', } export enum UserMetadataKey { - PREFERENCES = 'preferences', - LICENSE = 'license', - ONBOARDING = 'onboarding', + Preferences = 'preferences', + License = 'license', + Onboarding = 'onboarding', } export enum UserAvatarColor { - PRIMARY = 'primary', - PINK = 'pink', - RED = 'red', - YELLOW = 'yellow', - BLUE = 'blue', - GREEN = 'green', - PURPLE = 'purple', - ORANGE = 'orange', - GRAY = 'gray', - AMBER = 'amber', + Primary = 'primary', + Pink = 'pink', + Red = 'red', + Yellow = 'yellow', + Blue = 'blue', + Green = 'green', + Purple = 'purple', + Orange = 'orange', + Gray = 'gray', + Amber = 'amber', } export enum UserStatus { - ACTIVE = 'active', - REMOVING = 'removing', - DELETED = 'deleted', + Active = 'active', + Removing = 'removing', + Deleted = 'deleted', } export enum AssetStatus { - ACTIVE = 'active', - TRASHED = 'trashed', - DELETED = 'deleted', + Active = 'active', + Trashed = 'trashed', + Deleted = 'deleted', } export enum SourceType { - MACHINE_LEARNING = 'machine-learning', - EXIF = 'exif', - MANUAL = 'manual', + MachineLearning = 'machine-learning', + Exif = 'exif', + Manual = 'manual', } export enum ManualJobName { - PERSON_CLEANUP = 'person-cleanup', - TAG_CLEANUP = 'tag-cleanup', - USER_CLEANUP = 'user-cleanup', - MEMORY_CLEANUP = 'memory-cleanup', - MEMORY_CREATE = 'memory-create', - BACKUP_DATABASE = 'backup-database', + PersonCleanup = 'person-cleanup', + TagCleanup = 'tag-cleanup', + UserCleanup = 'user-cleanup', + MemoryCleanup = 'memory-cleanup', + MemoryCreate = 'memory-create', + BackupDatabase = 'backup-database', } export enum AssetPathType { - ORIGINAL = 'original', - FULLSIZE = 'fullsize', - PREVIEW = 'preview', - THUMBNAIL = 'thumbnail', - ENCODED_VIDEO = 'encoded_video', - SIDECAR = 'sidecar', + Original = 'original', + FullSize = 'fullsize', + Preview = 'preview', + Thumbnail = 'thumbnail', + EncodedVideo = 'encoded_video', + Sidecar = 'sidecar', } export enum PersonPathType { - FACE = 'face', + Face = 'face', } export enum UserPathType { - PROFILE = 'profile', + Profile = 'profile', } export type PathType = AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { - ALL = 'all', - OPTIMAL = 'optimal', - BITRATE = 'bitrate', - REQUIRED = 'required', - DISABLED = 'disabled', + All = 'all', + Optimal = 'optimal', + Bitrate = 'bitrate', + Required = 'required', + Disabled = 'disabled', } export enum TranscodeTarget { - NONE, - AUDIO, - VIDEO, - ALL, + None = 'NONE', + Audio = 'AUDIO', + Video = 'VIDEO', + All = 'ALL', } export enum VideoCodec { H264 = 'h264', - HEVC = 'hevc', - VP9 = 'vp9', - AV1 = 'av1', + Hevc = 'hevc', + Vp9 = 'vp9', + Av1 = 'av1', } export enum AudioCodec { - MP3 = 'mp3', - AAC = 'aac', - LIBOPUS = 'libopus', - PCMS16LE = 'pcm_s16le', + Mp3 = 'mp3', + Aac = 'aac', + LibOpus = 'libopus', + PcmS16le = 'pcm_s16le', } export enum VideoContainer { - MOV = 'mov', - MP4 = 'mp4', - OGG = 'ogg', - WEBM = 'webm', + Mov = 'mov', + Mp4 = 'mp4', + Ogg = 'ogg', + Webm = 'webm', } -export enum TranscodeHWAccel { - NVENC = 'nvenc', - QSV = 'qsv', - VAAPI = 'vaapi', - RKMPP = 'rkmpp', - DISABLED = 'disabled', +export enum TranscodeHardwareAcceleration { + Nvenc = 'nvenc', + Qsv = 'qsv', + Vaapi = 'vaapi', + Rkmpp = 'rkmpp', + Disabled = 'disabled', } export enum ToneMapping { - HABLE = 'hable', - MOBIUS = 'mobius', - REINHARD = 'reinhard', - DISABLED = 'disabled', + Hable = 'hable', + Mobius = 'mobius', + Reinhard = 'reinhard', + Disabled = 'disabled', } export enum CQMode { - AUTO = 'auto', - CQP = 'cqp', - ICQ = 'icq', + Auto = 'auto', + Cqp = 'cqp', + Icq = 'icq', } export enum Colorspace { - SRGB = 'srgb', + Srgb = 'srgb', P3 = 'p3', } export enum ImageFormat { - JPEG = 'jpeg', - WEBP = 'webp', + Jpeg = 'jpeg', + Webp = 'webp', } export enum RawExtractedFormat { - JPEG = 'jpeg', - JXL = 'jxl', + Jpeg = 'jpeg', + Jxl = 'jxl', } export enum LogLevel { - VERBOSE = 'verbose', - DEBUG = 'debug', - LOG = 'log', - WARN = 'warn', - ERROR = 'error', - FATAL = 'fatal', + Verbose = 'verbose', + Debug = 'debug', + Log = 'log', + Warn = 'warn', + Error = 'error', + Fatal = 'fatal', } export enum MetadataKey { - AUTH_ROUTE = 'auth_route', - ADMIN_ROUTE = 'admin_route', - SHARED_ROUTE = 'shared_route', - API_KEY_SECURITY = 'api_key', - EVENT_CONFIG = 'event_config', - JOB_CONFIG = 'job_config', - TELEMETRY_ENABLED = 'telemetry_enabled', + AuthRoute = 'auth_route', + AdminRoute = 'admin_route', + SharedRoute = 'shared_route', + ApiKeySecurity = 'api_key', + EventConfig = 'event_config', + JobConfig = 'job_config', + TelemetryEnabled = 'telemetry_enabled', } export enum RouteKey { - ASSET = 'assets', - USER = 'users', + Asset = 'assets', + User = 'users', } export enum CacheControl { - PRIVATE_WITH_CACHE = 'private_with_cache', - PRIVATE_WITHOUT_CACHE = 'private_without_cache', - NONE = 'none', -} - -export enum PaginationMode { - LIMIT_OFFSET = 'limit-offset', - SKIP_TAKE = 'skip-take', + PrivateWithCache = 'private_with_cache', + PrivateWithoutCache = 'private_without_cache', + None = 'none', } export enum ImmichEnvironment { - DEVELOPMENT = 'development', - TESTING = 'testing', - PRODUCTION = 'production', + Development = 'development', + Testing = 'testing', + Production = 'production', } export enum ImmichWorker { - API = 'api', - MICROSERVICES = 'microservices', + Api = 'api', + Microservices = 'microservices', } export enum ImmichTelemetry { - HOST = 'host', - API = 'api', - IO = 'io', - REPO = 'repo', - JOB = 'job', + Host = 'host', + Api = 'api', + Io = 'io', + Repo = 'repo', + Job = 'job', } export enum ExifOrientation { @@ -411,11 +407,11 @@ export enum ExifOrientation { } export enum DatabaseExtension { - CUBE = 'cube', - EARTH_DISTANCE = 'earthdistance', - VECTOR = 'vector', - VECTORS = 'vectors', - VECTORCHORD = 'vchord', + Cube = 'cube', + EarthDistance = 'earthdistance', + Vector = 'vector', + Vectors = 'vectors', + VectorChord = 'vchord', } export enum BootstrapEventPriority { @@ -428,135 +424,116 @@ export enum BootstrapEventPriority { } export enum QueueName { - THUMBNAIL_GENERATION = 'thumbnailGeneration', - METADATA_EXTRACTION = 'metadataExtraction', - VIDEO_CONVERSION = 'videoConversion', - FACE_DETECTION = 'faceDetection', - FACIAL_RECOGNITION = 'facialRecognition', - SMART_SEARCH = 'smartSearch', - DUPLICATE_DETECTION = 'duplicateDetection', - BACKGROUND_TASK = 'backgroundTask', - STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', - MIGRATION = 'migration', - SEARCH = 'search', - SIDECAR = 'sidecar', - LIBRARY = 'library', - NOTIFICATION = 'notifications', - BACKUP_DATABASE = 'backupDatabase', + ThumbnailGeneration = 'thumbnailGeneration', + MetadataExtraction = 'metadataExtraction', + VideoConversion = 'videoConversion', + FaceDetection = 'faceDetection', + FacialRecognition = 'facialRecognition', + SmartSearch = 'smartSearch', + DuplicateDetection = 'duplicateDetection', + BackgroundTask = 'backgroundTask', + StorageTemplateMigration = 'storageTemplateMigration', + Migration = 'migration', + Search = 'search', + Sidecar = 'sidecar', + Library = 'library', + Notification = 'notifications', + BackupDatabase = 'backupDatabase', } export enum JobName { - //backups - BACKUP_DATABASE = 'database-backup', + AssetDelete = 'AssetDelete', + AssetDeleteCheck = 'AssetDeleteCheck', + AssetDetectFacesQueueAll = 'AssetDetectFacesQueueAll', + AssetDetectFaces = 'AssetDetectFaces', + AssetDetectDuplicatesQueueAll = 'AssetDetectDuplicatesQueueAll', + AssetDetectDuplicates = 'AssetDetectDuplicates', + AssetEncodeVideoQueueAll = 'AssetEncodeVideoQueueAll', + AssetEncodeVideo = 'AssetEncodeVideo', + AssetEmptyTrash = 'AssetEmptyTrash', + AssetExtractMetadataQueueAll = 'AssetExtractMetadataQueueAll', + AssetExtractMetadata = 'AssetExtractMetadata', + AssetFileMigration = 'AssetFileMigration', + AssetGenerateThumbnailsQueueAll = 'AssetGenerateThumbnailsQueueAll', + AssetGenerateThumbnails = 'AssetGenerateThumbnails', - // conversion - QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', - VIDEO_CONVERSION = 'video-conversion', + AuditLogCleanup = 'AuditLogCleanup', - // thumbnails - QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_THUMBNAILS = 'generate-thumbnails', - GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', + DatabaseBackup = 'DatabaseBackup', - // metadata - QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', - METADATA_EXTRACTION = 'metadata-extraction', + FacialRecognitionQueueAll = 'FacialRecognitionQueueAll', + FacialRecognition = 'FacialRecognition', - // user - USER_DELETION = 'user-deletion', - USER_DELETE_CHECK = 'user-delete-check', - USER_SYNC_USAGE = 'user-sync-usage', + FileDelete = 'FileDelete', + FileMigrationQueueAll = 'FileMigrationQueueAll', - // asset - ASSET_DELETION = 'asset-deletion', - ASSET_DELETION_CHECK = 'asset-deletion-check', + LibraryDeleteCheck = 'LibraryDeleteCheck', + LibraryDelete = 'LibraryDelete', + LibraryRemoveAsset = 'LibraryRemoveAsset', + LibrarySyncAssetsQueueAll = 'LibraryScanAssetsQueueAll', + LibrarySyncAssets = 'LibrarySyncAssets', + LibrarySyncFilesQueueAll = 'LibrarySyncFilesQueueAll', + LibrarySyncFiles = 'LibrarySyncFiles', + LibraryScanQueueAll = 'LibraryScanQueueAll', - // storage template - STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', - STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', + MemoryCleanup = 'MemoryCleanup', + MemoryGenerate = 'MemoryGenerate', - // tags - TAG_CLEANUP = 'tag-cleanup', + NotificationsCleanup = 'NotificationsCleanup', - // migration - QUEUE_MIGRATION = 'queue-migration', - MIGRATE_ASSET = 'migrate-asset', - MIGRATE_PERSON = 'migrate-person', + NotifyUserSignup = 'NotifyUserSignup', + NotifyAlbumInvite = 'NotifyAlbumInvite', + NotifyAlbumUpdate = 'NotifyAlbumUpdate', - // facial recognition - PERSON_CLEANUP = 'person-cleanup', - QUEUE_FACE_DETECTION = 'queue-face-detection', - FACE_DETECTION = 'face-detection', - QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', - FACIAL_RECOGNITION = 'facial-recognition', + UserDelete = 'UserDelete', + UserDeleteCheck = 'UserDeleteCheck', + UserSyncUsage = 'UserSyncUsage', - // library management - LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files', - LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets', - LIBRARY_SYNC_FILES = 'library-sync-files', - LIBRARY_SYNC_ASSETS = 'library-sync-assets', - LIBRARY_ASSET_REMOVAL = 'handle-library-file-deletion', - LIBRARY_DELETE = 'library-delete', - LIBRARY_QUEUE_SCAN_ALL = 'library-queue-scan-all', - LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', + PersonCleanup = 'PersonCleanup', + PersonFileMigration = 'PersonFileMigration', + PersonGenerateThumbnail = 'PersonGenerateThumbnail', - // cleanup - DELETE_FILES = 'delete-files', - CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', - CLEAN_OLD_SESSION_TOKENS = 'clean-old-session-tokens', + SessionCleanup = 'SessionCleanup', - // memories - MEMORIES_CLEANUP = 'memories-cleanup', - MEMORIES_CREATE = 'memories-create', + SendMail = 'SendMail', - // smart search - QUEUE_SMART_SEARCH = 'queue-smart-search', - SMART_SEARCH = 'smart-search', + SidecarQueueAll = 'SidecarQueueAll', + SidecarDiscovery = 'SidecarDiscovery', + SidecarSync = 'SidecarSync', + SidecarWrite = 'SidecarWrite', - QUEUE_TRASH_EMPTY = 'queue-trash-empty', + SmartSearchQueueAll = 'SmartSearchQueueAll', + SmartSearch = 'SmartSearch', - // duplicate detection - QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection', - DUPLICATE_DETECTION = 'duplicate-detection', + StorageTemplateMigration = 'StorageTemplateMigration', + StorageTemplateMigrationSingle = 'StorageTemplateMigrationSingle', - // XMP sidecars - QUEUE_SIDECAR = 'queue-sidecar', - SIDECAR_DISCOVERY = 'sidecar-discovery', - SIDECAR_SYNC = 'sidecar-sync', - SIDECAR_WRITE = 'sidecar-write', + TagCleanup = 'TagCleanup', - // Notification - NOTIFY_SIGNUP = 'notify-signup', - NOTIFY_ALBUM_INVITE = 'notify-album-invite', - NOTIFY_ALBUM_UPDATE = 'notify-album-update', - NOTIFICATIONS_CLEANUP = 'notifications-cleanup', - SEND_EMAIL = 'notification-send-email', - - // Version check - VERSION_CHECK = 'version-check', + VersionCheck = 'VersionCheck', } export enum JobCommand { - START = 'start', - PAUSE = 'pause', - RESUME = 'resume', - EMPTY = 'empty', - CLEAR_FAILED = 'clear-failed', + Start = 'start', + Pause = 'pause', + Resume = 'resume', + Empty = 'empty', + ClearFailed = 'clear-failed', } export enum JobStatus { - SUCCESS = 'success', - FAILED = 'failed', - SKIPPED = 'skipped', + Success = 'success', + Failed = 'failed', + Skipped = 'skipped', } export enum QueueCleanType { - FAILED = 'failed', + Failed = 'failed', } export enum VectorIndex { - CLIP = 'clip_index', - FACE = 'face_index', + Clip = 'clip_index', + Face = 'face_index', } export enum DatabaseLock { @@ -568,6 +545,7 @@ export enum DatabaseLock { CLIPDimSize = 512, Library = 1337, NightlyJobs = 600, + MediaLocation = 700, GetSystemConfig = 69, BackupDatabase = 42, MemoryCreation = 777, @@ -590,6 +568,7 @@ export enum SyncRequestType { StacksV1 = 'StacksV1', UsersV1 = 'UsersV1', PeopleV1 = 'PeopleV1', + AssetFacesV1 = 'AssetFacesV1', UserMetadataV1 = 'UserMetadataV1', } @@ -641,6 +620,9 @@ export enum SyncEntityType { PersonV1 = 'PersonV1', PersonDeleteV1 = 'PersonDeleteV1', + AssetFaceV1 = 'AssetFaceV1', + AssetFaceDeleteV1 = 'AssetFaceDeleteV1', + UserMetadataV1 = 'UserMetadataV1', UserMetadataDeleteV1 = 'UserMetadataDeleteV1', @@ -663,8 +645,8 @@ export enum NotificationType { } export enum OAuthTokenEndpointAuthMethod { - CLIENT_SECRET_POST = 'client_secret_post', - CLIENT_SECRET_BASIC = 'client_secret_basic', + ClientSecretPost = 'client_secret_post', + ClientSecretBasic = 'client_secret_basic', } export enum DatabaseSslMode { @@ -676,14 +658,14 @@ export enum DatabaseSslMode { } export enum AssetVisibility { - ARCHIVE = 'archive', - TIMELINE = 'timeline', + Archive = 'archive', + Timeline = 'timeline', /** * Video part of the LivePhotos and MotionPhotos */ - HIDDEN = 'hidden', - LOCKED = 'locked', + Hidden = 'hidden', + Locked = 'locked', } export enum CronJob { diff --git a/server/src/main.ts b/server/src/main.ts index 95b35c6915..68ea396e7a 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,5 +1,6 @@ import { CommandFactory } from 'nest-commander'; import { ChildProcess, fork } from 'node:child_process'; +import { dirname, join } from 'node:path'; import { Worker } from 'node:worker_threads'; import { ImmichAdminModule } from 'src/app.module'; import { ImmichWorker, LogLevel } from 'src/enum'; @@ -20,7 +21,7 @@ const onExit = (name: string, exitCode: number | null) => { if (exitCode !== 0) { console.error(`${name} worker exited with code ${exitCode}`); - if (apiProcess && name !== ImmichWorker.API) { + if (apiProcess && name !== ImmichWorker.Api) { console.error('Killing api process'); apiProcess.kill('SIGTERM'); apiProcess = undefined; @@ -33,14 +34,18 @@ const onExit = (name: string, exitCode: number | null) => { function bootstrapWorker(name: ImmichWorker) { console.log(`Starting ${name} worker`); + // eslint-disable-next-line unicorn/prefer-module + const basePath = dirname(__filename); + const workerFile = join(basePath, 'workers', `${name}.js`); + let worker: Worker | ChildProcess; - if (name === ImmichWorker.API) { - worker = fork(`./dist/workers/${name}.js`, [], { + if (name === ImmichWorker.Api) { + worker = fork(workerFile, [], { execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)), }); apiProcess = worker; } else { - worker = new Worker(`./dist/workers/${name}.js`); + worker = new Worker(workerFile); } worker.on('error', (error) => onError(name, error)); @@ -50,7 +55,7 @@ function bootstrapWorker(name: ImmichWorker) { function bootstrap() { if (immichApp === 'immich-admin') { process.title = 'immich_admin_cli'; - process.env.IMMICH_LOG_LEVEL = LogLevel.WARN; + process.env.IMMICH_LOG_LEVEL = LogLevel.Warn; return CommandFactory.run(ImmichAdminModule); } diff --git a/server/src/middleware/asset-upload.interceptor.ts b/server/src/middleware/asset-upload.interceptor.ts index bc403ee562..0f1eaa4ce5 100644 --- a/server/src/middleware/asset-upload.interceptor.ts +++ b/server/src/middleware/asset-upload.interceptor.ts @@ -15,7 +15,7 @@ export class AssetUploadInterceptor implements NestInterceptor { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse>(); - const checksum = fromMaybeArray(req.headers[ImmichHeader.CHECKSUM]); + const checksum = fromMaybeArray(req.headers[ImmichHeader.Checksum]); const response = await this.service.getUploadAssetIdByChecksum(req.user, checksum); if (response) { res.status(200); diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 438843436b..238f99257a 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -23,12 +23,12 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator = const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), - ApiSecurity(MetadataKey.API_KEY_SECURITY), - SetMetadata(MetadataKey.AUTH_ROUTE, options || {}), + ApiSecurity(MetadataKey.ApiKeySecurity), + SetMetadata(MetadataKey.AuthRoute, options || {}), ]; if ((options as SharedLinkRoute)?.sharedLink) { - decorators.push(ApiQuery({ name: ImmichQuery.SHARED_LINK_KEY, type: String, required: false })); + decorators.push(ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false })); } return applyDecorators(...decorators); @@ -76,7 +76,7 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const targets = [context.getHandler()]; - const options = this.reflector.getAllAndOverride(MetadataKey.AUTH_ROUTE, targets); + const options = this.reflector.getAllAndOverride(MetadataKey.AuthRoute, targets); if (!options) { return true; } diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index b6f37dbbd2..59c28849e1 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -154,11 +154,11 @@ export class FileUploadInterceptor implements NestInterceptor { private getHandler(route: RouteKey) { switch (route) { - case RouteKey.ASSET: { + case RouteKey.Asset: { return this.handlers.assetUpload; } - case RouteKey.USER: { + case RouteKey.User: { return this.handlers.userProfile; } diff --git a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts index 68e1618775..2bd1acad34 100644 --- a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts +++ b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts @@ -6,7 +6,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddFaceSearchRelation1718486162779 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const vectorExtension = await getVectorExtension(queryRunner); - if (vectorExtension === DatabaseExtension.VECTORS) { + if (vectorExtension === DatabaseExtension.Vectors) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); } @@ -52,7 +52,7 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface { public async down(queryRunner: QueryRunner): Promise { const vectorExtension = await getVectorExtension(queryRunner); - if (vectorExtension === DatabaseExtension.VECTORS) { + if (vectorExtension === DatabaseExtension.Vectors) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); } diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index e482a38a9a..9425bd9a11 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -168,6 +168,30 @@ from where "livePhotoVideoId" = $1::uuid +-- AssetRepository.getFileSamples +select + "asset"."id", + "asset"."originalPath", + "asset"."sidecarPath", + "asset"."encodedVideoPath", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "path" + from + "asset_file" + where + "asset"."id" = "asset_file"."assetId" + ) as agg + ) as "files" +from + "asset" +limit + 3 + -- AssetRepository.getById select "asset".* diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 3e41edde9c..8ad5b96bbc 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -12,6 +12,17 @@ delete from "person" where "person"."id" in ($1) +-- PersonRepository.getFileSamples +select + "id", + "thumbnailPath" +from + "person" +where + "thumbnailPath" != '' +limit + 3 + -- PersonRepository.getAllForUser select "person".* diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 4782eedf1d..7502b79f57 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -409,6 +409,41 @@ where order by "updateId" asc +-- SyncRepository.assetFace.getDeletes +select + "asset_face_audit"."id", + "assetFaceId" +from + "asset_face_audit" + left join "asset" on "asset"."id" = "asset_face_audit"."assetId" +where + "asset"."ownerId" = $1 + and "asset_face_audit"."deletedAt" < now() - interval '1 millisecond' +order by + "asset_face_audit"."id" asc + +-- SyncRepository.assetFace.getUpserts +select + "asset_face"."id", + "assetId", + "personId", + "imageWidth", + "imageHeight", + "boundingBoxX1", + "boundingBoxY1", + "boundingBoxX2", + "boundingBoxY2", + "sourceType", + "asset_face"."updateId" +from + "asset_face" + left join "asset" on "asset"."id" = "asset_face"."assetId" +where + "asset_face"."updatedAt" < now() - interval '1 millisecond' + and "asset"."ownerId" = $1 +order by + "asset_face"."updateId" asc + -- SyncRepository.memory.getDeletes select "id", @@ -779,7 +814,6 @@ select "ownerId", "name", "birthDate", - "thumbnailPath", "isHidden", "isFavorite", "color", diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index f1809464bf..6a02654781 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -78,6 +78,17 @@ where "user"."isAdmin" = $1 and "user"."deletedAt" is null +-- UserRepository.getFileSamples +select + "id", + "profileImagePath" +from + "user" +where + "profileImagePath" != '' +limit + 3 + -- UserRepository.hasAdmin select "user"."id" diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 14a765778e..5cceb6dbe0 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -91,7 +91,7 @@ class AlbumAccess { } const accessRole = - access === AlbumUserRole.EDITOR ? [AlbumUserRole.EDITOR] : [AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]; + access === AlbumUserRole.Editor ? [AlbumUserRole.Editor] : [AlbumUserRole.Editor, AlbumUserRole.Viewer]; return this.db .selectFrom('album') @@ -178,7 +178,7 @@ class AssetAccess { .select('asset.id') .where('asset.id', 'in', [...assetIds]) .where('asset.ownerId', '=', userId) - .$if(!hasElevatedPermission, (eb) => eb.where('asset.visibility', '!=', AssetVisibility.LOCKED)) + .$if(!hasElevatedPermission, (eb) => eb.where('asset.visibility', '!=', AssetVisibility.Locked)) .execute() .then((assets) => new Set(assets.map((asset) => asset.id))); } @@ -200,8 +200,8 @@ class AssetAccess { .where('partner.sharedWithId', '=', userId) .where((eb) => eb.or([ - eb('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)), - eb('asset.visibility', '=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)), + eb('asset.visibility', '=', sql.lit(AssetVisibility.Hidden)), ]), ) diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 9b991ef17f..1a1104b118 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -90,7 +90,7 @@ export class ActivityRepository { .where('activity.albumId', '=', albumId) .where(({ or, and, eb }) => or([ - and([eb('asset.deletedAt', 'is', null), eb('asset.visibility', '!=', sql.lit(AssetVisibility.LOCKED))]), + and([eb('asset.deletedAt', 'is', null), eb('asset.visibility', '!=', sql.lit(AssetVisibility.Locked))]), eb('asset.id', 'is', null), ]), ) diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index d968ed100c..2fce797aff 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -24,7 +24,7 @@ export class AlbumUserRepository { .executeTakeFirstOrThrow(); } - @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.Viewer }] }) update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable) { return this.db .updateTable('album_user') diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index c784ae276f..0500bb867f 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -62,7 +62,7 @@ export class AssetJobRepository { .select(['asset.id', 'asset.thumbhash']) .select(withFiles) .where('asset.deletedAt', 'is', null) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .$if(!force, (qb) => qb // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails @@ -117,7 +117,7 @@ export class AssetJobRepository { .executeTakeFirst(); } - @GenerateSql({ params: [DummyValue.UUID, AssetFileType.THUMBNAIL] }) + @GenerateSql({ params: [DummyValue.UUID, AssetFileType.Thumbnail] }) getAlbumThumbnailFiles(id: string, fileType?: AssetFileType) { return this.db .selectFrom('asset_file') @@ -130,7 +130,7 @@ export class AssetJobRepository { private assetsWithPreviews() { return this.db .selectFrom('asset') - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .where('asset.deletedAt', 'is', null) .innerJoin('asset_job_status as job_status', 'assetId', 'asset.id') .where('job_status.previewAt', 'is not', null); @@ -167,7 +167,7 @@ export class AssetJobRepository { return this.db .selectFrom('asset') .select(['asset.id', 'asset.visibility']) - .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) + .select((eb) => withFiles(eb, AssetFileType.Preview)) .where('asset.id', '=', id) .executeTakeFirst(); } @@ -179,7 +179,7 @@ export class AssetJobRepository { .select(['asset.id', 'asset.visibility']) .$call(withExifInner) .select((eb) => withFaces(eb, true)) - .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) + .select((eb) => withFiles(eb, AssetFileType.Preview)) .where('asset.id', '=', id) .executeTakeFirst(); } @@ -225,7 +225,7 @@ export class AssetJobRepository { .select(['stack.id', 'stack.primaryAssetId']) .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) .where('stacked.deletedAt', 'is not', null) - .where('stacked.visibility', '=', AssetVisibility.TIMELINE) + .where('stacked.visibility', '=', AssetVisibility.Timeline) .whereRef('stacked.stackId', '=', 'stack.id') .groupBy('stack.id') .as('stacked_assets'), @@ -241,11 +241,11 @@ export class AssetJobRepository { return this.db .selectFrom('asset') .select(['asset.id']) - .where('asset.type', '=', AssetType.VIDEO) + .where('asset.type', '=', AssetType.Video) .$if(!force, (qb) => qb .where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')])) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN), + .where('asset.visibility', '!=', AssetVisibility.Hidden), ) .where('asset.deletedAt', 'is', null) .stream(); @@ -257,7 +257,7 @@ export class AssetJobRepository { .selectFrom('asset') .select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath']) .where('asset.id', '=', id) - .where('asset.type', '=', AssetType.VIDEO) + .where('asset.type', '=', AssetType.Video) .executeTakeFirst(); } @@ -327,7 +327,7 @@ export class AssetJobRepository { .$if(!force, (qb) => qb.where((eb) => eb.or([eb('asset.sidecarPath', '=', ''), eb('asset.sidecarPath', 'is', null)])), ) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .stream(); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index f00d0c170f..edbafaa22d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { Stack } from 'src/database'; @@ -230,13 +231,13 @@ export class AssetRepository { .where('asset_job_status.previewAt', 'is not', null) .where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) .where('asset.ownerId', '=', anyUuid(ownerIds)) - .where('asset.visibility', '=', AssetVisibility.TIMELINE) + .where('asset.visibility', '=', AssetVisibility.Timeline) .where((eb) => eb.exists((qb) => qb .selectFrom('asset_file') .whereRef('assetId', '=', 'asset.id') - .where('asset_file.type', '=', AssetFileType.PREVIEW), + .where('asset_file.type', '=', AssetFileType.Preview), ), ) .where('asset.deletedAt', 'is', null) @@ -318,7 +319,7 @@ export class AssetRepository { .select(['deviceAssetId']) .where('ownerId', '=', asUuid(ownerId)) .where('deviceId', '=', deviceId) - .where('visibility', '!=', AssetVisibility.HIDDEN) + .where('visibility', '!=', AssetVisibility.Hidden) .where('deletedAt', 'is', null) .execute(); @@ -335,6 +336,23 @@ export class AssetRepository { return count; } + @GenerateSql() + getFileSamples() { + return this.db + .selectFrom('asset') + .select((eb) => [ + 'asset.id', + 'asset.originalPath', + 'asset.sidecarPath', + 'asset.encodedVideoPath', + jsonArrayFrom(eb.selectFrom('asset_file').select('path').whereRef('asset.id', '=', 'asset_file.assetId')).as( + 'files', + ), + ]) + .limit(sql.lit(3)) + .execute(); + } + @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) { return this.db @@ -363,7 +381,7 @@ export class AssetRepository { .whereRef('stacked.stackId', '=', 'stack.id') .whereRef('stacked.id', '!=', 'stack.primaryAssetId') .where('stacked.deletedAt', 'is', null) - .where('stacked.visibility', '=', AssetVisibility.TIMELINE) + .where('stacked.visibility', '=', AssetVisibility.Timeline) .groupBy('stack.id') .as('stacked_assets'), (join) => join.on('stack.id', 'is not', null), @@ -463,15 +481,15 @@ export class AssetRepository { getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise { return this.db .selectFrom('asset') - .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO)) - .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.IMAGE).as(AssetType.IMAGE)) - .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO)) - .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER)) + .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.Audio).as(AssetType.Audio)) + .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.Image).as(AssetType.Image)) + .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.Video).as(AssetType.Video)) + .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.Other).as(AssetType.Other)) .where('ownerId', '=', asUuid(ownerId)) .$if(visibility === undefined, withDefaultVisibility) .$if(!!visibility, (qb) => qb.where('asset.visibility', '=', visibility!)) .$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!)) - .$if(!!isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) + .$if(!!isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .where('deletedAt', isTrashed ? 'is not' : 'is', null) .executeTakeFirstOrThrow(); } @@ -496,7 +514,7 @@ export class AssetRepository { qb .selectFrom('asset') .select(truncatedDate().as('timeBucket')) - .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) + .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility === undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) @@ -606,7 +624,7 @@ export class AssetRepository { .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) .whereRef('stacked.stackId', '=', 'asset.stackId') .where('stacked.deletedAt', 'is', null) - .where('stacked.visibility', '=', AssetVisibility.TIMELINE) + .where('stacked.visibility', '=', AssetVisibility.Timeline) .groupBy('stacked.stackId') .as('stacked_assets'), (join) => join.onTrue(), @@ -617,7 +635,7 @@ export class AssetRepository { .$if(options.isDuplicate !== undefined, (qb) => qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null), ) - .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) + .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) .orderBy('asset.fileCreatedAt', options.order ?? 'desc'), ) @@ -671,8 +689,8 @@ export class AssetRepository { .select(['assetId as data', 'asset_exif.city as value']) .$narrowType<{ value: NotNull }>() .where('ownerId', '=', asUuid(ownerId)) - .where('visibility', '=', AssetVisibility.TIMELINE) - .where('type', '=', AssetType.IMAGE) + .where('visibility', '=', AssetVisibility.Timeline) + .where('type', '=', AssetType.Image) .where('deletedAt', 'is', null) .limit(maxFields) .execute(); @@ -710,7 +728,7 @@ export class AssetRepository { ) .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')) .where('asset.ownerId', '=', asUuid(ownerId)) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .where('asset.updatedAt', '<=', updatedUntil) .$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!)) .orderBy('asset.id') @@ -738,7 +756,7 @@ export class AssetRepository { ) .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')) .where('asset.ownerId', '=', anyUuid(options.userIds)) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .where('asset.updatedAt', '>', options.updatedAfter) .limit(options.limit) .execute(); diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts index 85ca4a79f8..2d56eddc9a 100644 --- a/server/src/repositories/audit.repository.ts +++ b/server/src/repositories/audit.repository.ts @@ -18,7 +18,7 @@ export class AuditRepository { @GenerateSql({ params: [ DummyValue.DATE, - { action: DatabaseAction.CREATE, entityType: EntityType.ASSET, userIds: [DummyValue.UUID] }, + { action: DatabaseAction.Create, entityType: EntityType.Asset, userIds: [DummyValue.UUID] }, ], }) async getAfter(since: Date, options: AuditSearch): Promise { diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 238b48bcef..99cba43b99 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -13,6 +13,7 @@ const resetEnv = () => { 'IMMICH_WORKERS_EXCLUDE', 'IMMICH_TRUSTED_PROXIES', 'IMMICH_API_METRICS_PORT', + 'IMMICH_MEDIA_LOCATION', 'IMMICH_MICROSERVICES_METRICS_PORT', 'IMMICH_TELEMETRY_INCLUDE', 'IMMICH_TELEMETRY_EXCLUDE', @@ -76,6 +77,13 @@ describe('getEnv', () => { }); }); + describe('IMMICH_MEDIA_LOCATION', () => { + it('should throw an error for relative paths', () => { + process.env.IMMICH_MEDIA_LOCATION = './relative/path'; + expect(() => getEnv()).toThrowError('IMMICH_MEDIA_LOCATION must be an absolute path'); + }); + }); + describe('database', () => { it('should use defaults', () => { const { database } = getEnv(); @@ -95,7 +103,7 @@ describe('getEnv', () => { it('should validate DB_SSL_MODE', () => { process.env.DB_SSL_MODE = 'invalid'; - expect(() => getEnv()).toThrowError('Invalid environment variables: DB_SSL_MODE'); + expect(() => getEnv()).toThrowError('DB_SSL_MODE must be one of the following values:'); }); it('should accept a valid DB_SSL_MODE', () => { @@ -239,7 +247,7 @@ describe('getEnv', () => { it('should reject invalid trusted proxies', () => { process.env.IMMICH_TRUSTED_PROXIES = '10.1'; - expect(() => getEnv()).toThrowError('Invalid environment variables: IMMICH_TRUSTED_PROXIES'); + expect(() => getEnv()).toThrow('IMMICH_TRUSTED_PROXIES must be an ip address, or ip address range'); }); }); @@ -275,14 +283,14 @@ describe('getEnv', () => { process.env.IMMICH_TELEMETRY_EXCLUDE = 'job'; const { telemetry } = getEnv(); expect(telemetry.metrics).toEqual( - new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO, ImmichTelemetry.REPO]), + new Set([ImmichTelemetry.Api, ImmichTelemetry.Host, ImmichTelemetry.Io, ImmichTelemetry.Repo]), ); }); it('should run with specific telemetry metrics', () => { process.env.IMMICH_TELEMETRY_INCLUDE = 'io, host, api'; const { telemetry } = getEnv(); - expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO])); + expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.Api, ImmichTelemetry.Host, ImmichTelemetry.Io])); }); }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index dbb57bb141..c9e96a1803 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -131,12 +131,14 @@ const getEnv = (): EnvData => { const dto = plainToInstance(EnvDto, process.env); const errors = validateSync(dto); if (errors.length > 0) { - throw new Error( - `Invalid environment variables: ${errors.map((error) => `${error.property}=${error.value}`).join(', ')}`, - ); + const messages = [`Invalid environment variables: `]; + for (const error of errors) { + messages.push(` - ${error.property}=${error.value} (${Object.values(error.constraints || {}).join(', ')})`); + } + throw new Error(messages.join('\n')); } - const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]); + const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.Api, ImmichWorker.Microservices]); const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []); const workers = [...setDifference(includedWorkers, excludedWorkers)]; for (const worker of workers) { @@ -145,8 +147,8 @@ const getEnv = (): EnvData => { } } - const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION; - const isProd = environment === ImmichEnvironment.PRODUCTION; + const environment = dto.IMMICH_ENV || ImmichEnvironment.Production; + const isProd = environment === ImmichEnvironment.Production; const buildFolder = dto.IMMICH_BUILD_DATA || '/build'; const folders = { geodata: join(buildFolder, 'geodata'), @@ -199,15 +201,15 @@ const getEnv = (): EnvData => { let vectorExtension: VectorExtension | undefined; switch (dto.DB_VECTOR_EXTENSION) { case 'pgvector': { - vectorExtension = DatabaseExtension.VECTOR; + vectorExtension = DatabaseExtension.Vector; break; } case 'pgvecto.rs': { - vectorExtension = DatabaseExtension.VECTORS; + vectorExtension = DatabaseExtension.Vectors; break; } case 'vectorchord': { - vectorExtension = DatabaseExtension.VECTORCHORD; + vectorExtension = DatabaseExtension.VectorChord; break; } } @@ -254,11 +256,11 @@ const getEnv = (): EnvData => { mount: true, generateId: true, setup: (cls, req: Request, res: Response) => { - const headerValues = req.headers[ImmichHeader.CID]; + const headerValues = req.headers[ImmichHeader.Cid]; const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues; const cid = headerValue || cls.get(CLS_ID); cls.set(CLS_ID, cid); - res.header(ImmichHeader.CID, cid); + res.header(ImmichHeader.Cid, cid); }, }, }, @@ -278,9 +280,9 @@ const getEnv = (): EnvData => { otel: { metrics: { - hostMetrics: telemetries.has(ImmichTelemetry.HOST), + hostMetrics: telemetries.has(ImmichTelemetry.Host), apiMetrics: { - enable: telemetries.has(ImmichTelemetry.API), + enable: telemetries.has(ImmichTelemetry.Api), ignoreRoutes: excludePaths, }, }, @@ -335,7 +337,7 @@ export class ConfigRepository { } isDev() { - return this.getEnv().environment === ImmichEnvironment.DEVELOPMENT; + return this.getEnv().environment === ImmichEnvironment.Development; } getWorker() { diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index b1aefe19f8..1f83630cfa 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -53,8 +53,8 @@ export async function getVectorExtension(runner: Kysely | QueryRunner): Prom } export const probes: Record = { - [VectorIndex.CLIP]: 1, - [VectorIndex.FACE]: 1, + [VectorIndex.Clip]: 1, + [VectorIndex.Face]: 1, }; @Injectable() @@ -77,7 +77,7 @@ export class DatabaseRepository { return getVectorExtension(this.db); } - @GenerateSql({ params: [[DatabaseExtension.VECTORS]] }) + @GenerateSql({ params: [[DatabaseExtension.Vectors]] }) async getExtensionVersions(extensions: readonly DatabaseExtension[]): Promise { const { rows } = await sql` SELECT name, default_version as "availableVersion", installed_version as "installedVersion" @@ -89,13 +89,13 @@ export class DatabaseRepository { getExtensionVersionRange(extension: VectorExtension): string { switch (extension) { - case DatabaseExtension.VECTORCHORD: { + case DatabaseExtension.VectorChord: { return VECTORCHORD_VERSION_RANGE; } - case DatabaseExtension.VECTORS: { + case DatabaseExtension.Vectors: { return VECTORS_VERSION_RANGE; } - case DatabaseExtension.VECTOR: { + case DatabaseExtension.Vector: { return VECTOR_VERSION_RANGE; } default: { @@ -117,7 +117,7 @@ export class DatabaseRepository { async createExtension(extension: DatabaseExtension): Promise { this.logger.log(`Creating ${EXTENSION_NAMES[extension]} extension`); await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db); - if (extension === DatabaseExtension.VECTORCHORD) { + if (extension === DatabaseExtension.VectorChord) { const dbName = sql.id(await this.getDatabaseName()); await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db); await sql`SET vchordrq.probes = 1`.execute(this.db); @@ -147,8 +147,8 @@ export class DatabaseRepository { } await Promise.all([ - this.db.schema.dropIndex(VectorIndex.CLIP).ifExists().execute(), - this.db.schema.dropIndex(VectorIndex.FACE).ifExists().execute(), + this.db.schema.dropIndex(VectorIndex.Clip).ifExists().execute(), + this.db.schema.dropIndex(VectorIndex.Face).ifExists().execute(), ]); await this.db.transaction().execute(async (tx) => { @@ -156,14 +156,14 @@ export class DatabaseRepository { await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx); - if (extension === DatabaseExtension.VECTORS && (diff === 'major' || diff === 'minor')) { + if (extension === DatabaseExtension.Vectors && (diff === 'major' || diff === 'minor')) { await sql`SELECT pgvectors_upgrade()`.execute(tx); restartRequired = true; } }); if (!restartRequired) { - await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]); + await Promise.all([this.reindexVectors(VectorIndex.Clip), this.reindexVectors(VectorIndex.Face)]); } return { restartRequired }; @@ -171,7 +171,7 @@ export class DatabaseRepository { async prewarm(index: VectorIndex): Promise { const vectorExtension = await getVectorExtension(this.db); - if (vectorExtension !== DatabaseExtension.VECTORCHORD) { + if (vectorExtension !== DatabaseExtension.VectorChord) { return; } this.logger.debug(`Prewarming ${index}`); @@ -196,19 +196,19 @@ export class DatabaseRepository { } switch (vectorExtension) { - case DatabaseExtension.VECTOR: { + case DatabaseExtension.Vector: { if (!row.indexdef.toLowerCase().includes('using hnsw')) { promises.push(this.reindexVectors(indexName)); } break; } - case DatabaseExtension.VECTORS: { + case DatabaseExtension.Vectors: { if (!row.indexdef.toLowerCase().includes('using vectors')) { promises.push(this.reindexVectors(indexName)); } break; } - case DatabaseExtension.VECTORCHORD: { + case DatabaseExtension.VectorChord: { const matches = row.indexdef.match(/(?<=lists = \[)\d+/g); const lists = matches && matches.length > 0 ? Number(matches[0]) : 1; promises.push( @@ -264,7 +264,7 @@ export class DatabaseRepository { await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx); } await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE real[]`.execute(tx); - const schema = vectorExtension === DatabaseExtension.VECTORS ? 'vectors.' : ''; + const schema = vectorExtension === DatabaseExtension.Vectors ? 'vectors.' : ''; await sql` ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding @@ -329,11 +329,11 @@ export class DatabaseRepository { .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) .execute(); await sql - .raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: VectorIndex.CLIP })) + .raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: VectorIndex.Clip })) .execute(trx); await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); }); - probes[VectorIndex.CLIP] = 1; + probes[VectorIndex.Clip] = 1; await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db); } @@ -436,6 +436,39 @@ export class DatabaseRepository { this.logger.debug('Finished running kysely migrations'); } + async migrateFilePaths(sourceFolder: string, targetFolder: string): Promise { + // escaping regex special characters with a backslash + const sourceRegex = '^' + sourceFolder.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, String.raw`\$&`); + const source = sql.raw(`'${sourceRegex}'`); + const target = sql.lit(targetFolder); + + await this.db.transaction().execute(async (tx) => { + await tx + .updateTable('asset') + .set((eb) => ({ + originalPath: eb.fn('REGEXP_REPLACE', ['originalPath', source, target]), + encodedVideoPath: eb.fn('REGEXP_REPLACE', ['encodedVideoPath', source, target]), + sidecarPath: eb.fn('REGEXP_REPLACE', ['sidecarPath', source, target]), + })) + .execute(); + + await tx + .updateTable('asset_file') + .set((eb) => ({ path: eb.fn('REGEXP_REPLACE', ['path', source, target]) })) + .execute(); + + await tx + .updateTable('person') + .set((eb) => ({ thumbnailPath: eb.fn('REGEXP_REPLACE', ['thumbnailPath', source, target]) })) + .execute(); + + await tx + .updateTable('user') + .set((eb) => ({ profileImagePath: eb.fn('REGEXP_REPLACE', ['profileImagePath', source, target]) })) + .execute(); + }); + } + async withLock(lock: DatabaseLock, callback: () => Promise): Promise { let res; await this.asyncLock.acquire(DatabaseLock[lock], async () => { diff --git a/server/src/repositories/download.repository.ts b/server/src/repositories/download.repository.ts index 5645ca1217..ecc1e4d3ab 100644 --- a/server/src/repositories/download.repository.ts +++ b/server/src/repositories/download.repository.ts @@ -34,7 +34,7 @@ export class DownloadRepository { downloadUserId(userId: string) { return builder(this.db) .where('asset.ownerId', '=', userId) - .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.Hidden) .stream(); } } diff --git a/server/src/repositories/duplicate.repository.ts b/server/src/repositories/duplicate.repository.ts index ac9e5798e5..140c42a643 100644 --- a/server/src/repositories/duplicate.repository.ts +++ b/server/src/repositories/duplicate.repository.ts @@ -109,14 +109,14 @@ export class DuplicateRepository { assetId: DummyValue.UUID, embedding: DummyValue.VECTOR, maxDistance: 0.6, - type: AssetType.IMAGE, + type: AssetType.Image, userIds: [DummyValue.UUID], }, ], }) search({ assetId, embedding, maxDistance, type, userIds }: DuplicateSearch) { return this.db.transaction().execute(async (trx) => { - await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx); return await trx .with('cte', (qb) => qb diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 04494d5547..c1b26d5dde 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -166,7 +166,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect continue; } - const event = reflector.get(MetadataKey.EVENT_CONFIG, handler); + const event = reflector.get(MetadataKey.EventConfig, handler); if (!event) { continue; } diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 27c623cc89..5acd8d5746 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -41,7 +41,7 @@ export class JobRepository { const instance = this.moduleRef.get(Service); for (const methodName of getMethodNames(instance)) { const handler = instance[methodName]; - const config = reflector.get(MetadataKey.JOB_CONFIG, handler); + const config = reflector.get(MetadataKey.JobConfig, handler); if (!config) { continue; } @@ -99,7 +99,7 @@ export class JobRepository { const item = this.handlers[name as JobName]; if (!item) { this.logger.warn(`Skipping unknown job: "${name}"`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } return item.handler(data); @@ -205,20 +205,20 @@ export class JobRepository { private getJobOptions(item: JobItem): JobsOptions | null { switch (item.name) { - case JobName.NOTIFY_ALBUM_UPDATE: { + case JobName.NotifyAlbumUpdate: { return { jobId: `${item.data.id}/${item.data.recipientId}`, delay: item.data?.delay, }; } - case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { + case JobName.StorageTemplateMigrationSingle: { return { jobId: item.data.id }; } - case JobName.GENERATE_PERSON_THUMBNAIL: { + case JobName.PersonGenerateThumbnail: { return { priority: 1 }; } - case JobName.QUEUE_FACIAL_RECOGNITION: { - return { jobId: JobName.QUEUE_FACIAL_RECOGNITION }; + case JobName.FacialRecognitionQueueAll: { + return { jobId: JobName.FacialRecognitionQueueAll }; } default: { return null; diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 9f26191cfb..68102ab765 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -79,7 +79,7 @@ export class LibraryRepository { eb.fn .countAll() .filterWhere((eb) => - eb.and([eb('asset.type', '=', AssetType.IMAGE), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), + eb.and([eb('asset.type', '=', AssetType.Image), eb('asset.visibility', '!=', AssetVisibility.Hidden)]), ) .as('photos'), ) @@ -87,7 +87,7 @@ export class LibraryRepository { eb.fn .countAll() .filterWhere((eb) => - eb.and([eb('asset.type', '=', AssetType.VIDEO), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), + eb.and([eb('asset.type', '=', AssetType.Video), eb('asset.visibility', '!=', AssetVisibility.Hidden)]), ) .as('videos'), ) diff --git a/server/src/repositories/logging.repository.spec.ts b/server/src/repositories/logging.repository.spec.ts index 393eeb9496..99bb1dbf18 100644 --- a/server/src/repositories/logging.repository.spec.ts +++ b/server/src/repositories/logging.repository.spec.ts @@ -22,7 +22,7 @@ describe(LoggingRepository.name, () => { describe('formatContext', () => { it('should use colors', () => { sut = new LoggingRepository(clsMock, configMock); - sut.setAppName(ImmichWorker.API); + sut.setAppName(ImmichWorker.Api); const logger = new MyConsoleLogger(clsMock, { color: true }); @@ -31,7 +31,7 @@ describe(LoggingRepository.name, () => { it('should not use colors when color is false', () => { sut = new LoggingRepository(clsMock, configMock); - sut.setAppName(ImmichWorker.API); + sut.setAppName(ImmichWorker.Api); const logger = new MyConsoleLogger(clsMock, { color: false }); diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 2ac3715a50..1833168f3e 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -8,7 +8,7 @@ import { ConfigRepository } from 'src/repositories/config.repository'; type LogDetails = any; type LogFunction = () => string; -const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; +const LOG_LEVELS = [LogLevel.Verbose, LogLevel.Debug, LogLevel.Log, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal]; enum LogColor { RED = 31, @@ -20,7 +20,7 @@ enum LogColor { } let appName: string | undefined; -let logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; +let logLevels: LogLevel[] = [LogLevel.Log, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal]; export class MyConsoleLogger extends ConsoleLogger { private isColorEnabled: boolean; @@ -106,35 +106,35 @@ export class LoggingRepository { } verbose(message: string, ...details: LogDetails) { - this.handleMessage(LogLevel.VERBOSE, message, details); + this.handleMessage(LogLevel.Verbose, message, details); } verboseFn(message: LogFunction, ...details: LogDetails) { - this.handleFunction(LogLevel.VERBOSE, message, details); + this.handleFunction(LogLevel.Verbose, message, details); } debug(message: string, ...details: LogDetails) { - this.handleMessage(LogLevel.DEBUG, message, details); + this.handleMessage(LogLevel.Debug, message, details); } debugFn(message: LogFunction, ...details: LogDetails) { - this.handleFunction(LogLevel.DEBUG, message, details); + this.handleFunction(LogLevel.Debug, message, details); } log(message: string, ...details: LogDetails) { - this.handleMessage(LogLevel.LOG, message, details); + this.handleMessage(LogLevel.Log, message, details); } warn(message: string, ...details: LogDetails) { - this.handleMessage(LogLevel.WARN, message, details); + this.handleMessage(LogLevel.Warn, message, details); } error(message: string | Error, ...details: LogDetails) { - this.handleMessage(LogLevel.ERROR, message, details); + this.handleMessage(LogLevel.Error, message, details); } fatal(message: string, ...details: LogDetails) { - this.handleMessage(LogLevel.FATAL, message, details); + this.handleMessage(LogLevel.Fatal, message, details); } private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) { @@ -145,32 +145,32 @@ export class LoggingRepository { private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) { switch (level) { - case LogLevel.VERBOSE: { + case LogLevel.Verbose: { this.logger.verbose(message, ...details); break; } - case LogLevel.DEBUG: { + case LogLevel.Debug: { this.logger.debug(message, ...details); break; } - case LogLevel.LOG: { + case LogLevel.Log: { this.logger.log(message, ...details); break; } - case LogLevel.WARN: { + case LogLevel.Warn: { this.logger.warn(message, ...details); break; } - case LogLevel.ERROR: { + case LogLevel.Error: { this.logger.error(message, ...details); break; } - case LogLevel.FATAL: { + case LogLevel.Fatal: { this.logger.fatal(message, ...details); break; } diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 64c8a6229d..d1f60791c3 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -61,14 +61,14 @@ export class MapRepository { const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8'); // TODO move to service init - const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.ReverseGeocodingState); if (geocodingMetadata?.lastUpdate === geodataDate) { return; } await Promise.all([this.importGeodata(), this.importNaturalEarthCountries()]); - await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, { + await this.metadataRepository.set(SystemMetadataKey.ReverseGeocodingState, { lastUpdate: geodataDate, lastImportFileName: citiesFile, }); @@ -102,13 +102,13 @@ export class MapRepository { .$if(isArchived === true, (qb) => qb.where((eb) => eb.or([ - eb('asset.visibility', '=', AssetVisibility.TIMELINE), - eb('asset.visibility', '=', AssetVisibility.ARCHIVE), + eb('asset.visibility', '=', AssetVisibility.Timeline), + eb('asset.visibility', '=', AssetVisibility.Archive), ]), ), ) .$if(isArchived === false || isArchived === undefined, (qb) => - qb.where('asset.visibility', '=', AssetVisibility.TIMELINE), + qb.where('asset.visibility', '=', AssetVisibility.Timeline), ) .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 33cf4e3e03..6266acf0ed 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -55,28 +55,28 @@ export class MediaRepository { async extract(input: string): Promise { try { const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input); - return { buffer, format: RawExtractedFormat.JPEG }; + return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message); } try { const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input); - return { buffer, format: RawExtractedFormat.JPEG }; + return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message); } try { const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input); - return { buffer, format: RawExtractedFormat.JXL }; + return { buffer, format: RawExtractedFormat.Jxl }; } catch (error: any) { this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message); } try { const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input); - return { buffer, format: RawExtractedFormat.JPEG }; + return { buffer, format: RawExtractedFormat.Jpeg }; } catch (error: any) { this.logger.debug('Could not extract preview buffer from image', error.message); return null; @@ -142,7 +142,7 @@ export class MediaRepository { limitInputPixels: false, raw: options.raw, }) - .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16') + .pipelineColorspace(options.colorspace === Colorspace.Srgb ? 'srgb' : 'rgb16') .withIccProfile(options.colorspace); if (!options.raw) { @@ -267,7 +267,7 @@ export class MediaRepository { const { frameCount, percentInterval } = options.progress; const frameInterval = Math.ceil(frameCount / (100 / percentInterval)); - if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) { + if (this.logger.isLevelEnabled(LogLevel.Debug) && frameCount && frameInterval) { let lastProgressFrame: number = 0; ffmpegCall.on('progress', (progress: ProgressEvent) => { if (progress.frames - lastProgressFrame < frameInterval) { diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 7cf03508be..65b4cb3df7 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -19,7 +19,7 @@ export class MemoryRepository implements IBulkAsset { .deleteFrom('memory_asset') .using('asset') .whereRef('memory_asset.assetsId', '=', 'asset.id') - .where('asset.visibility', '!=', AssetVisibility.TIMELINE) + .where('asset.visibility', '!=', AssetVisibility.Timeline) .execute(); return this.db @@ -67,7 +67,7 @@ export class MemoryRepository implements IBulkAsset { .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') .whereRef('memory_asset.memoriesId', '=', 'memory.id') .orderBy('asset.fileCreatedAt', 'asc') - .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .where('asset.deletedAt', 'is', null), ).as('assets'), ) @@ -158,7 +158,7 @@ export class MemoryRepository implements IBulkAsset { .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') .whereRef('memory_asset.memoriesId', '=', 'memory.id') .orderBy('asset.fileCreatedAt', 'asc') - .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .where('asset.deletedAt', 'is', null), ).as('assets'), ) diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index e416a65249..2ea69eba27 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -48,7 +48,7 @@ export class MoveRepository { eb.selectFrom('asset').select('id').whereRef('asset.id', '=', 'move_history.entityId'), ), ) - .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .where('move_history.pathType', '=', sql.lit(AssetPathType.Original)) .execute(); } @@ -56,7 +56,7 @@ export class MoveRepository { async cleanMoveHistorySingle(assetId: string): Promise { await this.db .deleteFrom('move_history') - .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .where('move_history.pathType', '=', sql.lit(AssetPathType.Original)) .where('entityId', '=', assetId) .execute(); } diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index 357b52a77a..9a436e4b9a 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -138,11 +138,11 @@ export class OAuthRepository { } switch (tokenEndpointAuthMethod) { - case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST: { + case OAuthTokenEndpointAuthMethod.ClientSecretPost: { return ClientSecretPost(clientSecret); } - case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_BASIC: { + case OAuthTokenEndpointAuthMethod.ClientSecretBasic: { return ClientSecretBasic(clientSecret); } diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 1885a196ff..f653bb8179 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -142,6 +142,16 @@ export class PersonRepository { .stream(); } + @GenerateSql() + getFileSamples() { + return this.db + .selectFrom('person') + .select(['id', 'thumbnailPath']) + .where('thumbnailPath', '!=', sql.lit('')) + .limit(sql.lit(3)) + .execute(); + } + @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) { const items = await this.db @@ -151,7 +161,7 @@ export class PersonRepository { .innerJoin('asset', (join) => join .onRef('asset_face.assetId', '=', 'asset.id') - .on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .on('asset.deletedAt', 'is', null), ) .where('person.ownerId', '=', userId) @@ -276,7 +286,7 @@ export class PersonRepository { .selectFrom('asset_file') .select('asset_file.path') .whereRef('asset_file.assetId', '=', 'asset.id') - .where('asset_file.type', '=', sql.lit(AssetFileType.PREVIEW)) + .where('asset_file.type', '=', sql.lit(AssetFileType.Preview)) .as('previewPath'), ) .where('person.id', '=', id) @@ -341,7 +351,7 @@ export class PersonRepository { join .onRef('asset.id', '=', 'asset_face.assetId') .on('asset_face.personId', '=', personId) - .on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .on('asset.deletedAt', 'is', null), ) .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) @@ -369,7 +379,7 @@ export class PersonRepository { eb .selectFrom('asset') .whereRef('asset.id', '=', 'asset_face.assetId') - .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .where('asset.deletedAt', 'is', null), ), ), diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index fe8ad563bb..61e0cc1e29 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -256,7 +256,7 @@ export class SearchRepository { } return this.db.transaction().execute(async (trx) => { - await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx); const items = await searchAssetBuilder(trx, options) .selectAll('asset') .innerJoin('smart_search', 'asset.id', 'smart_search.assetId') @@ -284,7 +284,7 @@ export class SearchRepository { } return this.db.transaction().execute(async (trx) => { - await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.FACE])}`.execute(trx); + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Face])}`.execute(trx); return await trx .with('cte', (qb) => qb @@ -351,8 +351,8 @@ export class SearchRepository { .select(['city', 'assetId']) .innerJoin('asset', 'asset.id', 'asset_exif.assetId') .where('asset.ownerId', '=', anyUuid(userIds)) - .where('asset.visibility', '=', AssetVisibility.TIMELINE) - .where('asset.type', '=', AssetType.IMAGE) + .where('asset.visibility', '=', AssetVisibility.Timeline) + .where('asset.type', '=', AssetType.Image) .where('asset.deletedAt', 'is', null) .orderBy('city') .limit(1); @@ -367,8 +367,8 @@ export class SearchRepository { .select(['city', 'assetId']) .innerJoin('asset', 'asset.id', 'asset_exif.assetId') .where('asset.ownerId', '=', anyUuid(userIds)) - .where('asset.visibility', '=', AssetVisibility.TIMELINE) - .where('asset.type', '=', AssetType.IMAGE) + .where('asset.visibility', '=', AssetVisibility.Timeline) + .where('asset.type', '=', AssetType.Image) .where('asset.deletedAt', 'is', null) .whereRef('asset_exif.city', '>', 'cte.city') .orderBy('city') @@ -450,7 +450,7 @@ export class SearchRepository { .distinctOn(field) .innerJoin('asset', 'asset.id', 'asset_exif.assetId') .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '=', AssetVisibility.TIMELINE) + .where('visibility', '=', AssetVisibility.Timeline) .where('deletedAt', 'is', null) .where(field, 'is not', null); } diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index d61333fcd6..d5fb3be47d 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -103,7 +103,7 @@ export class SharedLinkRepository { .select((eb) => eb.fn.toJson('album').$castTo().as('album')) .where('shared_link.id', '=', id) .where('shared_link.userId', '=', userId) - .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)])) .orderBy('shared_link.createdAt', 'desc') .executeTakeFirst(); } @@ -165,7 +165,7 @@ export class SharedLinkRepository { (join) => join.onTrue(), ) .select((eb) => eb.fn.toJson('album').$castTo().as('album')) - .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)])) .$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!)) .orderBy('shared_link.createdAt', 'desc') .distinctOn(['shared_link.createdAt']) @@ -185,7 +185,7 @@ export class SharedLinkRepository { eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'), ).as('user'), ]) - .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)])) .executeTakeFirst(); } diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 34c450d52d..dba52d25a0 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -17,7 +17,8 @@ type AuditTables = | 'memory_asset_audit' | 'stack_audit' | 'person_audit' - | 'user_metadata_audit'; + | 'user_metadata_audit' + | 'asset_face_audit'; type UpsertTables = | 'user' | 'partner' @@ -29,7 +30,8 @@ type UpsertTables = | 'memory_asset' | 'stack' | 'person' - | 'user_metadata'; + | 'user_metadata' + | 'asset_face'; @Injectable() export class SyncRepository { @@ -40,6 +42,7 @@ export class SyncRepository { albumUser: AlbumUserSync; asset: AssetSync; assetExif: AssetExifSync; + assetFace: AssetFaceSync; memory: MemorySync; memoryToAsset: MemoryToAssetSync; partner: PartnerSync; @@ -59,6 +62,7 @@ export class SyncRepository { this.albumUser = new AlbumUserSync(this.db); this.asset = new AssetSync(this.db); this.assetExif = new AssetExifSync(this.db); + this.assetFace = new AssetFaceSync(this.db); this.memory = new MemorySync(this.db); this.memoryToAsset = new MemoryToAssetSync(this.db); this.partner = new PartnerSync(this.db); @@ -385,7 +389,6 @@ class PersonSync extends BaseSync { 'ownerId', 'name', 'birthDate', - 'thumbnailPath', 'isHidden', 'isFavorite', 'color', @@ -398,6 +401,46 @@ class PersonSync extends BaseSync { } } +class AssetFaceSync extends BaseSync { + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('asset_face_audit') + .select(['asset_face_audit.id', 'assetFaceId']) + .orderBy('asset_face_audit.id', 'asc') + .leftJoin('asset', 'asset.id', 'asset_face_audit.assetId') + .where('asset.ownerId', '=', userId) + .where('asset_face_audit.deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('asset_face_audit.id', '>', ack!.updateId)) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('asset_face') + .select([ + 'asset_face.id', + 'assetId', + 'personId', + 'imageWidth', + 'imageHeight', + 'boundingBoxX1', + 'boundingBoxY1', + 'boundingBoxX2', + 'boundingBoxY2', + 'sourceType', + 'asset_face.updateId', + ]) + .where('asset_face.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('asset_face.updateId', '>', ack!.updateId)) + .orderBy('asset_face.updateId', 'asc') + .leftJoin('asset', 'asset.id', 'asset_face.assetId') + .where('asset.ownerId', '=', userId) + .stream(); + } +} + class AssetExifSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index fc680ddcc5..5fbbb76cf7 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -112,21 +112,21 @@ export class TelemetryRepository { const { telemetry } = this.configRepository.getEnv(); const { metrics } = telemetry; - this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.API) }); - this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.HOST) }); - this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.JOB) }); - this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.REPO) }); + this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Api) }); + this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Host) }); + this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Job) }); + this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Repo) }); } setup({ repositories }: { repositories: ClassConstructor[] }) { const { telemetry } = this.configRepository.getEnv(); const { metrics } = telemetry; - if (!metrics.has(ImmichTelemetry.REPO)) { + if (!metrics.has(ImmichTelemetry.Repo)) { return; } for (const Repository of repositories) { - const isEnabled = this.reflect.get(MetadataKey.TELEMETRY_ENABLED, Repository) ?? true; + const isEnabled = this.reflect.get(MetadataKey.TelemetryEnabled, Repository) ?? true; if (!isEnabled) { this.logger.debug(`Telemetry disabled for ${Repository.name}`); continue; diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts index ee6fe3ace1..f6f13188d4 100644 --- a/server/src/repositories/trash.repository.ts +++ b/server/src/repositories/trash.repository.ts @@ -8,7 +8,7 @@ export class TrashRepository { constructor(@InjectKysely() private db: Kysely) {} getDeletedIds(): AsyncIterableIterator<{ id: string }> { - return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.DELETED).stream(); + return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.Deleted).stream(); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -16,8 +16,8 @@ export class TrashRepository { const { numUpdatedRows } = await this.db .updateTable('asset') .where('ownerId', '=', userId) - .where('status', '=', AssetStatus.TRASHED) - .set({ status: AssetStatus.ACTIVE, deletedAt: null }) + .where('status', '=', AssetStatus.Trashed) + .set({ status: AssetStatus.Active, deletedAt: null }) .executeTakeFirst(); return Number(numUpdatedRows); @@ -28,8 +28,8 @@ export class TrashRepository { const { numUpdatedRows } = await this.db .updateTable('asset') .where('ownerId', '=', userId) - .where('status', '=', AssetStatus.TRASHED) - .set({ status: AssetStatus.DELETED }) + .where('status', '=', AssetStatus.Trashed) + .set({ status: AssetStatus.Deleted }) .executeTakeFirst(); return Number(numUpdatedRows); @@ -43,9 +43,9 @@ export class TrashRepository { const { numUpdatedRows } = await this.db .updateTable('asset') - .where('status', '=', AssetStatus.TRASHED) + .where('status', '=', AssetStatus.Trashed) .where('id', 'in', ids) - .set({ status: AssetStatus.ACTIVE, deletedAt: null }) + .set({ status: AssetStatus.Active, deletedAt: null }) .executeTakeFirst(); return Number(numUpdatedRows); diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index f809280d86..9d5f19b26a 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -79,6 +79,16 @@ export class UserRepository { .executeTakeFirst(); } + @GenerateSql() + getFileSamples() { + return this.db + .selectFrom('user') + .select(['id', 'profileImagePath']) + .where('profileImagePath', '!=', sql.lit('')) + .limit(sql.lit(3)) + .execute(); + } + @GenerateSql() async hasAdmin(): Promise { const admin = await this.db @@ -187,7 +197,7 @@ export class UserRepository { restore(id: string) { return this.db .updateTable('user') - .set({ status: UserStatus.ACTIVE, deletedAt: null }) + .set({ status: UserStatus.Active, deletedAt: null }) .where('user.id', '=', asUuid(id)) .returning(columns.userAdmin) .returning(withMetadata) @@ -229,8 +239,8 @@ export class UserRepository { .countAll() .filterWhere((eb) => eb.and([ - eb('asset.type', '=', sql.lit(AssetType.IMAGE)), - eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.type', '=', sql.lit(AssetType.Image)), + eb('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)), ]), ) .as('photos'), @@ -238,8 +248,8 @@ export class UserRepository { .countAll() .filterWhere((eb) => eb.and([ - eb('asset.type', '=', sql.lit(AssetType.VIDEO)), - eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.type', '=', sql.lit(AssetType.Video)), + eb('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)), ]), ) .as('videos'), @@ -254,7 +264,7 @@ export class UserRepository { eb.fn .sum('asset_exif.fileSizeInByte') .filterWhere((eb) => - eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.IMAGE))]), + eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.Image))]), ), eb.lit(0), ) @@ -264,7 +274,7 @@ export class UserRepository { eb.fn .sum('asset_exif.fileSizeInByte') .filterWhere((eb) => - eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.VIDEO))]), + eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.Video))]), ), eb.lit(0), ) diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts index 0fd74d299f..93c1280191 100644 --- a/server/src/repositories/view-repository.ts +++ b/server/src/repositories/view-repository.ts @@ -15,7 +15,7 @@ export class ViewRepository { .select((eb) => eb.fn('substring', ['asset.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) .distinct() .where('ownerId', '=', asUuid(userId)) - .where('visibility', '=', AssetVisibility.TIMELINE) + .where('visibility', '=', AssetVisibility.Timeline) .where('deletedAt', 'is', null) .where('fileCreatedAt', 'is not', null) .where('fileModifiedAt', 'is not', null) @@ -34,7 +34,7 @@ export class ViewRepository { .selectAll('asset') .$call(withExif) .where('ownerId', '=', asUuid(userId)) - .where('visibility', '=', AssetVisibility.TIMELINE) + .where('visibility', '=', AssetVisibility.Timeline) .where('deletedAt', 'is', null) .where('fileCreatedAt', 'is not', null) .where('fileModifiedAt', 'is not', null) diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 5577169227..786e7a1ffa 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -229,3 +229,16 @@ export const user_metadata_audit = registerFunction({ RETURN NULL; END`, }); + +export const asset_face_audit = registerFunction({ + name: 'asset_face_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO asset_face_audit ("assetFaceId", "assetId") + SELECT "id", "assetId" + FROM OLD; + RETURN NULL; + END`, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index ba25a65d4d..8982437b34 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -4,6 +4,7 @@ import { album_user_after_insert, album_user_delete_audit, asset_delete_audit, + asset_face_audit, f_concat_ws, f_unaccent, immich_uuid_v7, @@ -27,6 +28,7 @@ import { AlbumTable } from 'src/schema/tables/album.table'; import { ApiKeyTable } from 'src/schema/tables/api-key.table'; import { AssetAuditTable } from 'src/schema/tables/asset-audit.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; @@ -78,6 +80,7 @@ export class ImmichDatabase { ApiKeyTable, AssetAuditTable, AssetFaceTable, + AssetFaceAuditTable, AssetJobStatusTable, AssetTable, AssetFileTable, @@ -132,6 +135,7 @@ export class ImmichDatabase { stack_delete_audit, person_delete_audit, user_metadata_audit, + asset_face_audit, ]; enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum]; @@ -158,6 +162,7 @@ export interface DB { asset: AssetTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; + asset_face_audit: AssetFaceAuditTable; asset_file: AssetFileTable; asset_job_status: AssetJobStatusTable; asset_audit: AssetAuditTable; diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index 63625a69ad..53a55d860e 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -16,9 +16,7 @@ export async function up(db: Kysely): Promise { rows: [lastMigration], } = await lastMigrationSql.execute(db); if (lastMigration?.name !== 'AddMissingIndex1744910873956') { - throw new Error( - 'Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade', - ); + throw new Error('Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade'); } logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration'); return; @@ -108,152 +106,344 @@ export async function up(db: Kysely): Promise { RETURN NULL; END; $$;`.execute(db); - if (vectorExtension === DatabaseExtension.VECTORS) { + if (vectorExtension === DatabaseExtension.Vectors) { await sql`SET search_path TO "$user", public, vectors`.execute(db); } await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db); await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db); - await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute(db); - await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute( + db, + ); + await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); await sql`COMMENT ON COLUMN "albums"."albumThumbnailAssetId" IS 'Asset ID to be used as thumbnail';`.execute(db); - await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); - await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute(db); - await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); - await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute(db); - await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute(db); - await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); - await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute( + db, + ); + await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute( + db, + ); + await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute( + db, + ); + await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute( + db, + ); + await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute( + db, + ); + await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute( + db, + ); + await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); await sql`CREATE TABLE "face_search" ("faceId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); - await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute(db); - await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute( + db, + ); + await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); await sql`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL);`.execute(db); - await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute(db); - await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute(db); - await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); - await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute(db); + await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute( + db, + ); + await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute( + db, + ); + await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute( + db, + ); + await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute( + db, + ); await sql`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "smart_search" ("assetId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); await sql`ALTER TABLE "smart_search" ALTER COLUMN "embedding" SET STORAGE EXTERNAL;`.execute(db); - await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); await sql`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); - await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); await sql`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL);`.execute(db); - await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); - await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); - await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute(db); + await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute( + db, + ); + await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute( + db, + ); + await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute( + db, + ); await sql`ALTER TABLE "users" ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "libraries" ADD CONSTRAINT "PK_505fedfcad00a09b3734b4223de" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "PK_da96729a8b113377cfb6a62439c" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "albums" ADD CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute(db); - await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute(db); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute( + db, + ); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute( + db, + ); await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "assets_audit" ADD CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "person" ADD CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute(db); + await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute( + db, + ); await sql`ALTER TABLE "audit" ADD CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId");`.execute(db); await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_pkey" PRIMARY KEY ("faceId");`.execute(db); - await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute( + db, + ); await sql`ALTER TABLE "memories" ADD CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute(db); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute( + db, + ); await sql`ALTER TABLE "move_history" ADD CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute(db); + await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute( + db, + ); + await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute( + db, + ); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute( + db, + ); await sql`ALTER TABLE "sessions" ADD CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute(db); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute( + db, + ); await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_pkey" PRIMARY KEY ("assetId");`.execute(db); - await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute(db); - await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute( + db, + ); + await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute( + db, + ); await sql`ALTER TABLE "tags" ADD CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute(db); - await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute(db); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute( + db, + ); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute( + db, + ); await sql`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute(db); - await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute(db); - await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute(db); - await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); - await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); - await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); - await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute(db); - await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); - await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); - await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute( + db, + ); + await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute( + db, + ); + await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute( + db, + ); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute( + db, + ); + await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute( + db, + ); + await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email");`.execute(db); await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" UNIQUE ("storageLabel");`.execute(db); - await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute(db); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute( + db, + ); await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type");`.execute(db); await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_newPath" UNIQUE ("newPath");`.execute(db); - await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute(db); + await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute( + db, + ); await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key");`.execute(db); await sql`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value");`.execute(db); - await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute(db); - await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute( + db, + ); + await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute( + db, + ); await sql`CREATE INDEX "IDX_users_updated_at_asc_id_asc" ON "users" ("updatedAt", "id")`.execute(db); await sql`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`.execute(db); await sql`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`.execute(db); - await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute( + db, + ); await sql`CREATE INDEX "IDX_asset_id_stackId" ON "assets" ("id", "stackId")`.execute(db); await sql`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`.execute(db); - await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute(db); + await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute( + db, + ); await sql`CREATE INDEX "idx_local_date_time" ON "assets" ((("localDateTime" at time zone 'UTC')::date))`.execute(db); - await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute(db); - await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute(db); + await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute( + db, + ); + await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute( + db, + ); await sql`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`.execute(db); await sql`CREATE INDEX "idx_asset_file_created_at" ON "assets" ("fileCreatedAt")`.execute(db); await sql`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum")`.execute(db); @@ -266,7 +456,9 @@ export async function up(db: Kysely): Promise { await sql`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`.execute(db); await sql`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`.execute(db); - await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute(db); + await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute( + db, + ); await sql`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`.execute(db); await sql`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`.execute(db); await sql`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`.execute(db); @@ -295,11 +487,21 @@ export async function up(db: Kysely): Promise { await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db); await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db); await sql.raw(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })).execute(db); - await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(db); - await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(db); - await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(db); - await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute(db); - await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute( + db, + ); + await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute( + db, + ); + await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute( + db, + ); + await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute( + db, + ); + await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute( + db, + ); await sql`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId")`.execute(db); @@ -319,7 +521,9 @@ export async function up(db: Kysely): Promise { await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db); await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(db); await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db); - await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute( + db, + ); await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db); await sql`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`.execute(db); await sql`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`.execute(db); @@ -407,5 +611,5 @@ export async function up(db: Kysely): Promise { } export async function down(): Promise { -// not implemented + // not implemented } diff --git a/server/src/schema/migrations/1749067526135-UserOnboardingDefault.ts b/server/src/schema/migrations/1749067526135-UserOnboardingDefault.ts index 376541410f..e6aabec27d 100644 --- a/server/src/schema/migrations/1749067526135-UserOnboardingDefault.ts +++ b/server/src/schema/migrations/1749067526135-UserOnboardingDefault.ts @@ -2,11 +2,11 @@ import { Kysely, sql } from 'kysely'; import { UserMetadataKey } from 'src/enum'; export async function up(db: Kysely): Promise { - await sql`INSERT INTO user_metadata SELECT id, ${UserMetadataKey.ONBOARDING}, '{"isOnboarded": true}' FROM users + await sql`INSERT INTO user_metadata SELECT id, ${UserMetadataKey.Onboarding}, '{"isOnboarded": true}' FROM users ON CONFLICT ("userId", key) DO NOTHING `.execute(db); } export async function down(db: Kysely): Promise { - await sql`DELETE FROM user_metadata WHERE key = ${UserMetadataKey.ONBOARDING}`.execute(db); + await sql`DELETE FROM user_metadata WHERE key = ${UserMetadataKey.Onboarding}`.execute(db); } diff --git a/server/src/schema/migrations/1752759108283-ConvertToAbsolutePaths.ts b/server/src/schema/migrations/1752759108283-ConvertToAbsolutePaths.ts new file mode 100644 index 0000000000..68b0c7931e --- /dev/null +++ b/server/src/schema/migrations/1752759108283-ConvertToAbsolutePaths.ts @@ -0,0 +1,39 @@ +import { Kysely, sql } from 'kysely'; +import { LoggingRepository } from 'src/repositories/logging.repository'; + +const logger = LoggingRepository.create(); +logger.setContext('Migrations'); + +export async function up(db: Kysely): Promise { + if (process.env.IMMICH_MEDIA_LOCATION) { + // do not automatically convert paths for a custom location/setting + return; + } + + // we construct paths using `path.join(mediaLocation, ...)`, which strips the leading './' + const source = 'upload'; + const target = '/usr/src/app/upload'; + + logger.log(`Converting database file paths from relative to absolute (source=${source}/*, target=${target}/*)`); + + // escaping regex special characters with a backslash + const sourceRegex = '^' + source.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, String.raw`\$&`); + + const items: Array<{ table: string; column: string }> = [ + { table: 'asset', column: 'originalPath' }, + { table: 'asset', column: 'encodedVideoPath' }, + { table: 'asset', column: 'sidecarPath' }, + { table: 'asset_file', column: 'path' }, + { table: 'person', column: 'thumbnailPath' }, + { table: 'user', column: 'profileImagePath' }, + ]; + + for (const { table, column } of items) { + const query = `UPDATE "${table}" SET "${column}" = REGEXP_REPLACE("${column}", '${sourceRegex}', '${target}') WHERE "${column}" IS NOT NULL`; + await sql.raw(query).execute(db); + } +} + +export async function down(): Promise { + // not supported +} diff --git a/server/src/schema/migrations/1753104909784-AssetFaceUpdateIdAndAuditTable.ts b/server/src/schema/migrations/1753104909784-AssetFaceUpdateIdAndAuditTable.ts new file mode 100644 index 0000000000..1f4072e34e --- /dev/null +++ b/server/src/schema/migrations/1753104909784-AssetFaceUpdateIdAndAuditTable.ts @@ -0,0 +1,52 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_face_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_face_audit ("assetFaceId", "assetId") + SELECT "id", "assetId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "asset_face_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "assetFaceId" uuid NOT NULL, + "assetId" uuid NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "asset_face_audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "asset_face_audit_assetFaceId_idx" ON "asset_face_audit" ("assetFaceId");`.execute(db); + await sql`CREATE INDEX "asset_face_audit_assetId_idx" ON "asset_face_audit" ("assetId");`.execute(db); + await sql`CREATE INDEX "asset_face_audit_deletedAt_idx" ON "asset_face_audit" ("deletedAt");`.execute(db); + await sql`ALTER TABLE "asset_face" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db); + await sql`ALTER TABLE "asset_face" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_face_audit" + AFTER DELETE ON "asset_face" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_face_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_face_updatedAt" + BEFORE UPDATE ON "asset_face" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_face_audit', '{"type":"function","name":"asset_face_audit","sql":"CREATE OR REPLACE FUNCTION asset_face_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_face_audit (\\"assetFaceId\\", \\"assetId\\")\\n SELECT \\"id\\", \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_face_audit', '{"type":"trigger","name":"asset_face_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_face_audit\\"\\n AFTER DELETE ON \\"asset_face\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_face_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_face_updatedAt', '{"type":"trigger","name":"asset_face_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_face_updatedAt\\"\\n BEFORE UPDATE ON \\"asset_face\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_face_audit" ON "asset_face";`.execute(db); + await sql`DROP TRIGGER "asset_face_updatedAt" ON "asset_face";`.execute(db); + await sql`ALTER TABLE "asset_face" DROP COLUMN "updatedAt";`.execute(db); + await sql`ALTER TABLE "asset_face" DROP COLUMN "updateId";`.execute(db); + await sql`DROP TABLE "asset_face_audit";`.execute(db); + await sql`DROP FUNCTION asset_face_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_face_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_face_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_face_updatedAt';`.execute(db); +} diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 6f20e25d90..94383218da 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -47,7 +47,7 @@ export class AlbumUserTable { }) usersId!: string; - @Column({ type: 'character varying', default: AlbumUserRole.EDITOR }) + @Column({ type: 'character varying', default: AlbumUserRole.Editor }) role!: Generated; @CreateIdColumn({ index: true }) diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index bca15d520b..5628db3d03 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -57,7 +57,7 @@ export class AlbumTable { @Column({ type: 'boolean', default: true }) isActivityEnabled!: Generated; - @Column({ default: AssetOrder.DESC }) + @Column({ default: AssetOrder.Desc }) order!: Generated; @UpdateIdColumn({ index: true }) diff --git a/server/src/schema/tables/asset-face-audit.table.ts b/server/src/schema/tables/asset-face-audit.table.ts new file mode 100644 index 0000000000..4f03c22aa0 --- /dev/null +++ b/server/src/schema/tables/asset-face-audit.table.ts @@ -0,0 +1,17 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; + +@Table('asset_face_audit') +export class AssetFaceAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + assetFaceId!: string; + + @Column({ type: 'uuid', index: true }) + assetId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 483d655768..5041d945e2 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -1,8 +1,11 @@ +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { SourceType } from 'src/enum'; import { asset_face_source_type } from 'src/schema/enums'; +import { asset_face_audit } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { + AfterDeleteTrigger, Column, DeleteDateColumn, ForeignKeyColumn, @@ -11,9 +14,17 @@ import { PrimaryGeneratedColumn, Table, Timestamp, + UpdateDateColumn, } from 'src/sql-tools'; @Table({ name: 'asset_face' }) +@UpdatedAtTrigger('asset_face_updatedAt') +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_face_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) // schemaFromDatabase does not preserve column order @Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] }) @Index({ columns: ['personId', 'assetId'] }) @@ -56,9 +67,15 @@ export class AssetFaceTable { @Column({ default: 0, type: 'integer' }) boundingBoxY2!: Generated; - @Column({ default: SourceType.MACHINE_LEARNING, enum: asset_face_source_type }) + @Column({ default: SourceType.MachineLearning, enum: asset_face_source_type }) sourceType!: Generated; @DeleteDateColumn() deletedAt!: Timestamp | null; + + @UpdateDateColumn() + updatedAt!: Generated; + + @UpdateIdColumn() + updateId!: Generated; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 4e1d073848..e92e01a1bd 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -132,12 +132,12 @@ export class AssetTable { @Column({ type: 'uuid', nullable: true, index: true }) duplicateId!: string | null; - @Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE }) + @Column({ enum: assets_status_enum, default: AssetStatus.Active }) status!: Generated; @UpdateIdColumn({ index: true }) updateId!: Generated; - @Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE }) + @Column({ enum: asset_visibility_enum, default: AssetVisibility.Timeline }) visibility!: Generated; } diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 97ac0ff295..46d6656382 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -73,7 +73,7 @@ export class UserTable { @Column({ type: 'bigint', default: 0 }) quotaUsageInBytes!: Generated>; - @Column({ type: 'character varying', default: UserStatus.ACTIVE }) + @Column({ type: 'character varying', default: UserStatus.Active }) status!: Generated; @Column({ type: 'timestamp with time zone', default: () => 'now()' }) diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 8256a34f02..b1c25f8286 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -18,7 +18,7 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class ActivityService extends BaseService { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] }); const activities = await this.activityRepository.search({ userId: dto.userId, albumId: dto.albumId, @@ -30,12 +30,12 @@ export class ActivityService extends BaseService { } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] }); return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }); } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { - await this.requireAccess({ auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); + await this.requireAccess({ auth, permission: Permission.ActivityCreate, ids: [dto.albumId] }); const common = { userId: auth.user.id, @@ -69,7 +69,7 @@ export class ActivityService extends BaseService { } async delete(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.ActivityDelete, ids: [id] }); await this.activityRepository.delete(id); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index cdace249c0..6f07a31dd9 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -146,7 +146,7 @@ describe(AlbumService.name, () => { await sut.create(authStub.admin, { albumName: 'Empty album', - albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], + albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }], description: '', assetIds: ['123'], }); @@ -160,7 +160,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: '123', }, ['123'], - [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], + [{ userId: 'user-id', role: AlbumUserRole.Editor }], ); expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); @@ -177,10 +177,10 @@ describe(AlbumService.name, () => { mocks.user.get.mockResolvedValue(userStub.user1); mocks.user.getMetadata.mockResolvedValue([ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { albums: { - defaultAssetOrder: AssetOrder.ASC, + defaultAssetOrder: AssetOrder.Asc, }, }, }, @@ -189,7 +189,7 @@ describe(AlbumService.name, () => { await sut.create(authStub.admin, { albumName: 'Empty album', - albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], + albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }], description: '', assetIds: ['123'], }); @@ -203,7 +203,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: '123', }, ['123'], - [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], + [{ userId: 'user-id', role: AlbumUserRole.Editor }], ); expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); @@ -220,7 +220,7 @@ describe(AlbumService.name, () => { await expect( sut.create(authStub.admin, { albumName: 'Empty album', - albumUsers: [{ userId: 'user-3', role: AlbumUserRole.EDITOR }], + albumUsers: [{ userId: 'user-3', role: AlbumUserRole.Editor }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.user.get).toHaveBeenCalledWith('user-3', {}); @@ -262,7 +262,7 @@ describe(AlbumService.name, () => { await expect( sut.create(authStub.admin, { albumName: 'Empty album', - albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.EDITOR }], + albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.Editor }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.create).not.toHaveBeenCalled(); @@ -404,7 +404,7 @@ describe(AlbumService.name, () => { mocks.albumUser.create.mockResolvedValue({ usersId: userStub.user2.id, albumsId: albumStub.sharedWithAdmin.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }); await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: authStub.user2.user.id }], @@ -512,11 +512,11 @@ describe(AlbumService.name, () => { mocks.albumUser.update.mockResolvedValue(null as any); await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, { - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }); expect(mocks.albumUser.update).toHaveBeenCalledWith( { albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id }, - { role: AlbumUserRole.EDITOR }, + { role: AlbumUserRole.Editor }, ); }); }); @@ -585,7 +585,7 @@ describe(AlbumService.name, () => { expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( authStub.user1.user.id, new Set(['album-123']), - AlbumUserRole.VIEWER, + AlbumUserRole.Viewer, ); }); @@ -596,7 +596,7 @@ describe(AlbumService.name, () => { expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set(['album-123']), - AlbumUserRole.VIEWER, + AlbumUserRole.Viewer, ); }); }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 88f264f4b3..a2edbb0384 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -79,7 +79,7 @@ export class AlbumService extends BaseService { } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -110,7 +110,7 @@ export class AlbumService extends BaseService { const allowedAssetIdsSet = await this.checkAccess({ auth, - permission: Permission.ASSET_SHARE, + permission: Permission.AssetShare, ids: dto.assetIds || [], }); const assetIds = [...allowedAssetIdsSet].map((id) => id); @@ -137,7 +137,7 @@ export class AlbumService extends BaseService { } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] }); const album = await this.findOrFail(id, { withAssets: true }); @@ -160,13 +160,13 @@ export class AlbumService extends BaseService { } async delete(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumDelete, ids: [id] }); await this.albumRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.requireAccess({ auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumAddAsset, ids: [id] }); const results = await addAssets( auth, @@ -195,13 +195,13 @@ export class AlbumService extends BaseService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumRemoveAsset, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( auth, { access: this.accessRepository, bulk: this.albumRepository }, - { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, + { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.AlbumDelete }, ); const removedIds = results.filter(({ success }) => success).map(({ id }) => id); @@ -213,7 +213,7 @@ export class AlbumService extends BaseService { } async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); @@ -257,14 +257,14 @@ export class AlbumService extends BaseService { // non-admin can remove themselves if (auth.user.id !== userId) { - await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] }); } await this.albumUserRepository.delete({ albumsId: id, usersId: userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise { - await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] }); await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); } diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 3448b4330f..fffe7bb536 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -15,7 +15,7 @@ describe(ApiKeyService.name, () => { describe('create', () => { it('should create a new key', async () => { const auth = factory.auth(); - const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.ALL] }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.All] }); const key = 'super-secret'; mocks.crypto.randomBytesAsText.mockReturnValue(key); @@ -41,12 +41,12 @@ describe(ApiKeyService.name, () => { mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.apiKey.create.mockResolvedValue(apiKey); - await sut.create(auth, { permissions: [Permission.ALL] }); + await sut.create(auth, { permissions: [Permission.All] }); expect(mocks.apiKey.create).toHaveBeenCalledWith({ key: 'super-secret (hashed)', name: 'API Key', - permissions: [Permission.ALL], + permissions: [Permission.All], userId: auth.user.id, }); expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled(); @@ -54,9 +54,9 @@ describe(ApiKeyService.name, () => { }); it('should throw an error if the api key does not have sufficient permissions', async () => { - const auth = factory.auth({ apiKey: { permissions: [Permission.ASSET_READ] } }); + const auth = factory.auth({ apiKey: { permissions: [Permission.AssetRead] } }); - await expect(sut.create(auth, { permissions: [Permission.ASSET_UPDATE] })).rejects.toBeInstanceOf( + await expect(sut.create(auth, { permissions: [Permission.AssetUpdate] })).rejects.toBeInstanceOf( BadRequestException, ); }); @@ -69,7 +69,7 @@ describe(ApiKeyService.name, () => { mocks.apiKey.getById.mockResolvedValue(void 0); - await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.ALL] })).rejects.toBeInstanceOf( + await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.All] })).rejects.toBeInstanceOf( BadRequestException, ); @@ -84,18 +84,18 @@ describe(ApiKeyService.name, () => { mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey); - await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.ALL] }); + await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.All] }); expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { name: newName, - permissions: [Permission.ALL], + permissions: [Permission.All], }); }); it('should update permissions', async () => { const auth = factory.auth(); const apiKey = factory.apiKey({ userId: auth.user.id }); - const newPermissions = [Permission.ACTIVITY_CREATE, Permission.ACTIVITY_READ, Permission.ACTIVITY_UPDATE]; + const newPermissions = [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate]; mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey); diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index bb8f7115b8..0585a159ac 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -157,7 +157,7 @@ const assetEntity = Object.freeze({ ownerId: 'user_id_1', deviceAssetId: 'device_asset_id_1', deviceId: 'device_id_1', - type: AssetType.VIDEO, + type: AssetType.Video, originalPath: 'fake_path/asset_1.jpeg', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -177,7 +177,7 @@ const assetEntity = Object.freeze({ const existingAsset = Object.freeze({ ...assetEntity, duration: null, - type: AssetType.IMAGE, + type: AssetType.Image, checksum: Buffer.from('_getExistingAsset', 'utf8'), libraryId: 'libraryId', originalFileName: 'existing-filename.jpeg', @@ -294,16 +294,16 @@ describe(AssetMediaService.name, () => { it('should return profile for profile uploads', () => { expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual( - 'upload/profile/admin_id', + expect.stringContaining('upload/profile/admin_id'), ); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/profile/admin_id'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/profile/admin_id')); }); it('should return upload for everything else', () => { expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual( - 'upload/upload/admin_id/ra/nd', + expect.stringContaining('upload/upload/admin_id/ra/nd'), ); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/upload/admin_id/ra/nd'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/upload/admin_id/ra/nd')); }); }); @@ -384,7 +384,7 @@ describe(AssetMediaService.name, () => { }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: ['fake_path/asset_1.jpeg', undefined] }, }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); @@ -409,7 +409,7 @@ describe(AssetMediaService.name, () => { ); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: ['fake_path/asset_1.jpeg', undefined] }, }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); @@ -437,7 +437,7 @@ describe(AssetMediaService.name, () => { it('should hide the linked motion asset', async () => { mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); @@ -455,7 +455,7 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); }); @@ -505,8 +505,9 @@ describe(AssetMediaService.name, () => { await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).resolves.toEqual( new ImmichFileResponse({ path: '/original/path.jpg', + fileName: 'asset-id.jpg', contentType: 'image/jpeg', - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, }), ); }); @@ -546,7 +547,7 @@ describe(AssetMediaService.name, () => { { id: '42', path: '/path/to/preview', - type: AssetFileType.THUMBNAIL, + type: AssetFileType.Thumbnail, }, ], }); @@ -563,7 +564,7 @@ describe(AssetMediaService.name, () => { { id: '42', path: '/path/to/preview.jpg', - type: AssetFileType.PREVIEW, + type: AssetFileType.Preview, }, ], }); @@ -573,7 +574,7 @@ describe(AssetMediaService.name, () => { ).resolves.toEqual( new ImmichFileResponse({ path: '/path/to/preview.jpg', - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', fileName: 'asset-id_thumbnail.jpg', }), @@ -588,7 +589,7 @@ describe(AssetMediaService.name, () => { ).resolves.toEqual( new ImmichFileResponse({ path: '/uploads/user-id/thumbs/path.jpg', - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', fileName: 'asset-id_preview.jpg', }), @@ -603,7 +604,7 @@ describe(AssetMediaService.name, () => { ).resolves.toEqual( new ImmichFileResponse({ path: '/uploads/user-id/webp/path.ext', - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', fileName: 'asset-id_thumbnail.ext', }), @@ -640,7 +641,7 @@ describe(AssetMediaService.name, () => { await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual( new ImmichFileResponse({ path: assetStub.hasEncodedVideo.encodedVideoPath!, - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, contentType: 'video/mp4', }), ); @@ -653,7 +654,7 @@ describe(AssetMediaService.name, () => { await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( new ImmichFileResponse({ path: assetStub.video.originalPath, - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', }), ); @@ -723,7 +724,7 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { deletedAt: expect.any(Date), - status: AssetStatus.TRASHED, + status: AssetStatus.Trashed, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.storage.utimes).toHaveBeenCalledWith( @@ -754,7 +755,7 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { deletedAt: expect.any(Date), - status: AssetStatus.TRASHED, + status: AssetStatus.Trashed, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.storage.utimes).toHaveBeenCalledWith( @@ -783,7 +784,7 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { deletedAt: expect.any(Date), - status: AssetStatus.TRASHED, + status: AssetStatus.Trashed, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.storage.utimes).toHaveBeenCalledWith( @@ -815,7 +816,7 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [updatedFile.originalPath, undefined] }, }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); @@ -912,8 +913,8 @@ describe(AssetMediaService.name, () => { await sut.onUploadError(request, file); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.DELETE_FILES, - data: { files: ['upload/upload/user-id/ra/nd/random-uuid.jpg'] }, + name: JobName.FileDelete, + data: { files: [expect.stringContaining('upload/upload/user-id/ra/nd/random-uuid.jpg')] }, }); }); }); diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 6fc438481d..517a1f665f 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -106,9 +106,9 @@ export class AssetMediaService extends BaseService { getUploadFolder({ auth, fieldName, file }: UploadRequest): string { auth = requireUploadAccess(auth); - let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); + let folder = StorageCore.getNestedFolder(StorageFolder.Upload, auth.user.id, file.uuid); if (fieldName === UploadFieldName.PROFILE_DATA) { - folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, auth.user.id); + folder = StorageCore.getFolderLocation(StorageFolder.Profile, auth.user.id); } this.storageRepository.mkdirSync(folder); @@ -121,7 +121,7 @@ export class AssetMediaService extends BaseService { const uploadFolder = this.getUploadFolder(asRequest(request, file)); const uploadPath = `${uploadFolder}/${uploadFilename}`; - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [uploadPath] } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [uploadPath] } }); } async uploadAsset( @@ -133,7 +133,7 @@ export class AssetMediaService extends BaseService { try { await this.requireAccess({ auth, - permission: Permission.ASSET_UPLOAD, + permission: Permission.AssetUpload, // do not need an id here, but the interface requires it ids: [auth.user.id], }); @@ -164,7 +164,7 @@ export class AssetMediaService extends BaseService { sidecarFile?: UploadFile, ): Promise { try { - await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); const asset = await this.assetRepository.getById(id); if (!asset) { @@ -179,7 +179,7 @@ export class AssetMediaService extends BaseService { // but the local variable holds the original file data paths. const copiedPhoto = await this.createCopy(asset); // and immediate trash it - await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED }); + await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.Trashed }); await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id }); await this.userRepository.updateUsage(auth.user.id, file.size); @@ -191,14 +191,15 @@ export class AssetMediaService extends BaseService { } async downloadOriginal(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] }); const asset = await this.findOrFail(id); return new ImmichFileResponse({ path: asset.originalPath, + fileName: asset.originalFileName, contentType: mimeTypes.lookup(asset.originalPath), - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, }); } @@ -207,7 +208,7 @@ export class AssetMediaService extends BaseService { id: string, dto: AssetMediaOptionsDto, ): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; @@ -240,16 +241,16 @@ export class AssetMediaService extends BaseService { fileName, path: filepath, contentType: mimeTypes.lookup(filepath), - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, }); } async playbackVideo(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] }); const asset = await this.findOrFail(id); - if (asset.type !== AssetType.VIDEO) { + if (asset.type !== AssetType.Video) { throw new BadRequestException('Asset is not a video'); } @@ -258,7 +259,7 @@ export class AssetMediaService extends BaseService { return new ImmichFileResponse({ path: filepath, contentType: mimeTypes.lookup(filepath), - cacheControl: CacheControl.PRIVATE_WITH_CACHE, + cacheControl: CacheControl.PrivateWithCache, }); } @@ -312,7 +313,7 @@ export class AssetMediaService extends BaseService { ): Promise { // clean up files await this.jobRepository.queue({ - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [file.originalPath, sidecarFile?.originalPath] }, }); @@ -365,7 +366,7 @@ export class AssetMediaService extends BaseService { await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size }); await this.jobRepository.queue({ - name: JobName.METADATA_EXTRACTION, + name: JobName.AssetExtractMetadata, data: { id: assetId, source: 'upload' }, }); } @@ -394,7 +395,7 @@ export class AssetMediaService extends BaseService { const { size } = await this.storageRepository.stat(created.originalPath); await this.assetRepository.upsertExif({ assetId: created.id, fileSizeInByte: size }); - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: created.id, source: 'copy' } }); + await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: created.id, source: 'copy' } }); return created; } @@ -416,7 +417,7 @@ export class AssetMediaService extends BaseService { type: mimeTypes.assetType(file.originalPath), isFavorite: dto.isFavorite, duration: dto.duration || null, - visibility: dto.visibility ?? AssetVisibility.TIMELINE, + visibility: dto.visibility ?? AssetVisibility.Timeline, livePhotoVideoId: dto.livePhotoVideoId, originalFileName: dto.filename || file.originalName, sidecarPath: sidecarFile?.originalPath, @@ -427,7 +428,7 @@ export class AssetMediaService extends BaseService { } await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); + await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } }); return asset; } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 65b14ca2da..6461735976 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -13,10 +13,10 @@ import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; const stats: AssetStats = { - [AssetType.IMAGE]: 10, - [AssetType.VIDEO]: 23, - [AssetType.AUDIO]: 0, - [AssetType.OTHER]: 0, + [AssetType.Image]: 10, + [AssetType.Video]: 23, + [AssetType.Audio]: 0, + [AssetType.Other]: 0, }; const statResponse: AssetStatsResponseDto = { @@ -46,21 +46,21 @@ describe(AssetService.name, () => { describe('getStatistics', () => { it('should get the statistics for a user, excluding archived assets', async () => { mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual( + await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Timeline })).resolves.toEqual( statResponse, ); expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); }); it('should get the statistics for a user for archived assets', async () => { mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual( + await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Archive })).resolves.toEqual( statResponse, ); expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { - visibility: AssetVisibility.ARCHIVE, + visibility: AssetVisibility.Archive, }); }); @@ -202,7 +202,7 @@ describe(AssetService.name, () => { describe('update', () => { it('should require asset write access for the id', async () => { await expect( - sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }), + sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.Timeline }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalled(); @@ -253,7 +253,7 @@ describe(AssetService.name, () => { }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { assetId: assetStub.livePhotoMotionAsset.id, @@ -277,7 +277,7 @@ describe(AssetService.name, () => { }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { assetId: assetStub.livePhotoMotionAsset.id, @@ -301,7 +301,7 @@ describe(AssetService.name, () => { }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { assetId: assetStub.livePhotoMotionAsset.id, @@ -314,7 +314,7 @@ describe(AssetService.name, () => { mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, ownerId: authStub.admin.user.id, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); mocks.asset.getById.mockResolvedValueOnce(assetStub.image); mocks.asset.update.mockResolvedValue(assetStub.image); @@ -325,7 +325,7 @@ describe(AssetService.name, () => { expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { assetId: assetStub.livePhotoMotionAsset.id, @@ -392,10 +392,10 @@ describe(AssetService.name, () => { it('should update all assets', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE }); + await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive }); expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { - visibility: AssetVisibility.ARCHIVE, + visibility: AssetVisibility.Archive, }); }); @@ -428,7 +428,7 @@ describe(AssetService.name, () => { expect(mocks.asset.updateAll).toHaveBeenCalled(); expect(mocks.asset.updateAllExif).toHaveBeenCalledWith(['asset-1'], { latitude: 0, longitude: 0 }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SIDECAR_WRITE, data: { id: 'asset-1', latitude: 0, longitude: 0 } }, + { name: JobName.SidecarWrite, data: { id: 'asset-1', latitude: 0, longitude: 0 } }, ]); }); @@ -451,7 +451,7 @@ describe(AssetService.name, () => { longitude: 50, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SIDECAR_WRITE, data: { id: 'asset-1', dateTimeOriginal, latitude: 30, longitude: 50 } }, + { name: JobName.SidecarWrite, data: { id: 'asset-1', dateTimeOriginal, latitude: 30, longitude: 50 } }, ]); }); @@ -497,7 +497,7 @@ describe(AssetService.name, () => { expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], { deletedAt: expect.any(Date), - status: AssetStatus.TRASHED, + status: AssetStatus.Trashed, }); expect(mocks.job.queue.mock.calls).toEqual([]); }); @@ -518,11 +518,11 @@ describe(AssetService.name, () => { mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset])); mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: false } }); - await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(new Date()); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION, data: { id: asset.id, deleteOnDisk: true } }, + { name: JobName.AssetDelete, data: { id: asset.id, deleteOnDisk: true } }, ]); }); @@ -532,11 +532,11 @@ describe(AssetService.name, () => { mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset])); mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: true, days: 7 } }); - await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(DateTime.now().minus({ days: 7 }).toJSDate()); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION, data: { id: asset.id, deleteOnDisk: true } }, + { name: JobName.AssetDelete, data: { id: asset.id, deleteOnDisk: true } }, ]); }); }); @@ -552,7 +552,7 @@ describe(AssetService.name, () => { expect(mocks.job.queue.mock.calls).toEqual([ [ { - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [ '/uploads/user-id/webp/path.ext', @@ -606,7 +606,7 @@ describe(AssetService.name, () => { expect(mocks.job.queue.mock.calls).toEqual([ [ { - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: assetStub.livePhotoMotionAsset.id, deleteOnDisk: true, @@ -615,7 +615,7 @@ describe(AssetService.name, () => { ], [ { - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [ '/uploads/user-id/webp/path.ext', @@ -643,7 +643,7 @@ describe(AssetService.name, () => { expect(mocks.job.queue.mock.calls).toEqual([ [ { - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [ '/uploads/user-id/webp/path.ext', @@ -668,7 +668,7 @@ describe(AssetService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0); await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe( - JobStatus.FAILED, + JobStatus.Failed, ); }); }); @@ -679,7 +679,7 @@ describe(AssetService.name, () => { await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetDetectFaces, data: { id: 'asset-1' } }]); }); it('should run the refresh metadata job', async () => { @@ -687,7 +687,9 @@ describe(AssetService.name, () => { await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { name: JobName.AssetExtractMetadata, data: { id: 'asset-1' } }, + ]); }); it('should run the refresh thumbnails job', async () => { @@ -695,7 +697,9 @@ describe(AssetService.name, () => { await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, + ]); }); it('should run the transcode video', async () => { @@ -703,7 +707,7 @@ describe(AssetService.name, () => { await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetEncodeVideo, data: { id: 'asset-1' } }]); }); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 351e8827dd..864a9cc512 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -23,7 +23,7 @@ import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUn @Injectable() export class AssetService extends BaseService { async getStatistics(auth: AuthDto, dto: AssetStatsDto) { - if (dto.visibility === AssetVisibility.LOCKED) { + if (dto.visibility === AssetVisibility.Locked) { requireElevatedPermission(auth); } @@ -46,7 +46,7 @@ export class AssetService extends BaseService { } async get(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); const asset = await this.assetRepository.getById(id, { exifInfo: true, @@ -78,7 +78,7 @@ export class AssetService extends BaseService { } async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const repos = { asset: this.assetRepository, event: this.eventRepository }; @@ -114,7 +114,7 @@ export class AssetService extends BaseService { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto; - await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids }); + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids }); if ( description !== undefined || @@ -125,7 +125,7 @@ export class AssetService extends BaseService { await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude }); await this.jobRepository.queueAll( ids.map((id) => ({ - name: JobName.SIDECAR_WRITE, + name: JobName.SidecarWrite, data: { id, description, dateTimeOriginal, latitude, longitude }, })), ); @@ -139,13 +139,13 @@ export class AssetService extends BaseService { ) { await this.assetRepository.updateAll(ids, options); - if (options.visibility === AssetVisibility.LOCKED) { + if (options.visibility === AssetVisibility.Locked) { await this.albumRepository.removeAssetsFromAll(ids); } } } - @OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.AssetDeleteCheck, queue: QueueName.BackgroundTask }) async handleAssetDeletionCheck(): Promise { const config = await this.getConfig({ withCache: false }); const trashedDays = config.trash.enabled ? config.trash.days : 0; @@ -158,7 +158,7 @@ export class AssetService extends BaseService { if (chunk.length > 0) { await this.jobRepository.queueAll( chunk.map(({ id, isOffline }) => ({ - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id, deleteOnDisk: !isOffline }, })), ); @@ -176,17 +176,17 @@ export class AssetService extends BaseService { await queueChunk(); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.ASSET_DELETION, queue: QueueName.BACKGROUND_TASK }) - async handleAssetDeletion(job: JobOf): Promise { + @OnJob({ name: JobName.AssetDelete, queue: QueueName.BackgroundTask }) + async handleAssetDeletion(job: JobOf): Promise { const { id, deleteOnDisk } = job; const asset = await this.assetJobRepository.getForAssetDeletion(id); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } // Replace the parent of the stack children with a new asset @@ -215,7 +215,7 @@ export class AssetService extends BaseService { const count = await this.assetRepository.getLivePhotoCount(asset.livePhotoVideoId); if (count === 0) { await this.jobRepository.queue({ - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: asset.livePhotoVideoId, deleteOnDisk }, }); } @@ -228,18 +228,18 @@ export class AssetService extends BaseService { files.push(asset.sidecarPath, asset.originalPath); } - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files } }); - return JobStatus.SUCCESS; + return JobStatus.Success; } async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids }); + await this.requireAccess({ auth, permission: Permission.AssetDelete, ids }); await this.assetRepository.updateAll(ids, { deletedAt: new Date(), - status: force ? AssetStatus.DELETED : AssetStatus.TRASHED, + status: force ? AssetStatus.Deleted : AssetStatus.Trashed, }); await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', { assetIds: ids, @@ -248,29 +248,29 @@ export class AssetService extends BaseService { } async run(auth: AuthDto, dto: AssetJobsDto) { - await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); const jobs: JobItem[] = []; for (const id of dto.assetIds) { switch (dto.name) { case AssetJobName.REFRESH_FACES: { - jobs.push({ name: JobName.FACE_DETECTION, data: { id } }); + jobs.push({ name: JobName.AssetDetectFaces, data: { id } }); break; } case AssetJobName.REFRESH_METADATA: { - jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } }); + jobs.push({ name: JobName.AssetExtractMetadata, data: { id } }); break; } case AssetJobName.REGENERATE_THUMBNAIL: { - jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id } }); + jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id } }); break; } case AssetJobName.TRANSCODE_VIDEO: { - jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id } }); + jobs.push({ name: JobName.AssetEncodeVideo, data: { id } }); break; } } @@ -292,7 +292,7 @@ export class AssetService extends BaseService { const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); if (Object.keys(writes).length > 0) { await this.assetRepository.upsertExif({ assetId: id, ...writes }); - await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id, ...writes } }); } } } diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index 381b2ec7e8..7363ea74e1 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -18,7 +18,7 @@ describe(AuditService.name, () => { it('should delete old audit entries', async () => { mocks.audit.removeBefore.mockResolvedValue(); - await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleCleanup()).resolves.toBe(JobStatus.Success); expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); }); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 7c9a070dd0..498d99b82c 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -7,9 +7,9 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class AuditService extends BaseService { - @OnJob({ name: JobName.CLEAN_OLD_AUDIT_LOGS, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.AuditLogCleanup, queue: QueueName.BackgroundTask }) async handleCleanup(): Promise { await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); - return JobStatus.SUCCESS; + return JobStatus.Success; } } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 93bd265ba0..129877bbdd 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -154,7 +154,7 @@ describe(AuthService.name, () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); - await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({ successful: true, redirectUri: 'http://end-session-endpoint', }); @@ -163,7 +163,7 @@ describe(AuthService.name, () => { it('should return the default redirect', async () => { const auth = factory.auth(); - await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -173,7 +173,7 @@ describe(AuthService.name, () => { const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto; mocks.session.delete.mockResolvedValue(); - await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -185,7 +185,7 @@ describe(AuthService.name, () => { it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { const auth = { user: { id: '123' } } as AuthDto; - await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -463,7 +463,7 @@ describe(AuthService.name, () => { sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, queryParams: {}, - metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: Permission.ASSET_READ }, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: Permission.AssetRead }, }), ).rejects.toBeInstanceOf(ForbiddenException); }); @@ -789,7 +789,7 @@ describe(AuthService.name, () => { ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { - profileImagePath: `upload/profile/${user.id}/${fileId}.jpg`, + profileImagePath: expect.stringContaining(`upload/profile/${user.id}/${fileId}.jpg`), profileChangedAt: expect.any(Date), }); expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(pictureUrl); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index a7b0cb3259..a5b0de25cd 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -194,13 +194,13 @@ export class AuthService extends BaseService { } private async validate({ headers, queryParams }: Omit): Promise { - const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || queryParams[ImmichQuery.SHARED_LINK_KEY]) as string; - const session = (headers[ImmichHeader.USER_TOKEN] || - headers[ImmichHeader.SESSION_TOKEN] || - queryParams[ImmichQuery.SESSION_KEY] || + const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string; + const session = (headers[ImmichHeader.UserToken] || + headers[ImmichHeader.SessionToken] || + queryParams[ImmichQuery.SessionKey] || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[ImmichHeader.API_KEY] || queryParams[ImmichQuery.API_KEY]) as string; + const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string; if (shareKey) { return this.validateSharedLink(shareKey); @@ -321,7 +321,7 @@ export class AuthService extends BaseService { const { contentType, data } = await this.oauthRepository.getProfilePicture(url); const extensionWithDot = mimeTypes.toExtension(contentType || 'image/jpeg') ?? 'jpg'; const profileImagePath = join( - StorageCore.getFolderLocation(StorageFolder.PROFILE, user.id), + StorageCore.getFolderLocation(StorageFolder.Profile, user.id), `${this.cryptoRepository.randomUUID()}${extensionWithDot}`, ); @@ -330,7 +330,7 @@ export class AuthService extends BaseService { await this.userRepository.update(user.id, { profileImagePath, profileChangedAt: new Date() }); if (oldPath) { - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldPath] } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldPath] } }); } } catch (error: Error | any) { this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack); @@ -366,7 +366,7 @@ export class AuthService extends BaseService { } private async getLogoutEndpoint(authType: AuthType): Promise { - if (authType !== AuthType.OAUTH) { + if (authType !== AuthType.OAuth) { return LOGIN_URL; } @@ -389,17 +389,17 @@ export class AuthService extends BaseService { private getCookieToken(headers: IncomingHttpHeaders): string | null { const cookies = parse(headers.cookie || ''); - return cookies[ImmichCookie.ACCESS_TOKEN] || null; + return cookies[ImmichCookie.AccessToken] || null; } private getCookieOauthState(headers: IncomingHttpHeaders): string | null { const cookies = parse(headers.cookie || ''); - return cookies[ImmichCookie.OAUTH_STATE] || null; + return cookies[ImmichCookie.OAuthState] || null; } private getCookieCodeVerifier(headers: IncomingHttpHeaders): string | null { const cookies = parse(headers.cookie || ''); - return cookies[ImmichCookie.OAUTH_CODE_VERIFIER] || null; + return cookies[ImmichCookie.OAuthCodeVerifier] || null; } async validateSharedLink(key: string | string[]): Promise { diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index aa72fd588a..e36f699f53 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -38,7 +38,7 @@ describe(BackupService.name, () => { }); it('should not initialise backup database job when running on microservices', async () => { - mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); expect(mocks.cron.create).not.toHaveBeenCalled(); @@ -98,10 +98,10 @@ describe(BackupService.name, () => { await sut.cleanupDatabaseBackups(); expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-123.sql.gz.tmp`, + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`, ); expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-345.sql.gz.tmp`, + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-345.sql.gz.tmp`, ); }); @@ -111,7 +111,7 @@ describe(BackupService.name, () => { await sut.cleanupDatabaseBackups(); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz`, + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`, ); }); @@ -125,10 +125,10 @@ describe(BackupService.name, () => { await sut.cleanupDatabaseBackups(); expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz.tmp`, + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz.tmp`, ); expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-2.sql.gz`, + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-2.sql.gz`, ); }); }); @@ -145,13 +145,13 @@ describe(BackupService.name, () => { it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.SUCCESS); + expect(result).toBe(JobStatus.Success); expect(mocks.storage.createWriteStream).toHaveBeenCalled(); }); it('should rename file on success', async () => { const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.SUCCESS); + expect(result).toBe(JobStatus.Success); expect(mocks.storage.rename).toHaveBeenCalled(); }); @@ -219,7 +219,7 @@ describe(BackupService.name, () => { mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); const result = await sut.handleBackupDatabase(); expect(mocks.process.spawn).not.toHaveBeenCalled(); - expect(result).toBe(JobStatus.FAILED); + expect(result).toBe(JobStatus.Failed); }); }); }); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 79c5deee57..9daa2c3aea 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -14,7 +14,7 @@ import { handlePromiseError } from 'src/utils/misc'; export class BackupService extends BaseService { private backupLock = false; - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] }) + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) async onConfigInit({ newConfig: { backup: { database }, @@ -26,7 +26,7 @@ export class BackupService extends BaseService { this.cronRepository.create({ name: 'backupDatabase', expression: database.cronExpression, - onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.BACKUP_DATABASE }), this.logger), + onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger), start: database.enabled, }); } @@ -51,7 +51,7 @@ export class BackupService extends BaseService { backup: { database: config }, } = await this.getConfig({ withCache: false }); - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.BACKUPS); + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); const files = await this.storageRepository.readdir(backupsFolder); const failedBackups = files.filter((file) => file.match(/immich-db-backup-\d+\.sql\.gz\.tmp$/)); const backups = files @@ -68,7 +68,7 @@ export class BackupService extends BaseService { this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); } - @OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE }) + @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) async handleBackupDatabase(): Promise { this.logger.debug(`Database Backup Started`); const { database } = this.configRepository.getEnv(); @@ -92,7 +92,7 @@ export class BackupService extends BaseService { databaseParams.push('--clean', '--if-exists'); const databaseVersion = await this.databaseRepository.getPostgresVersion(); const backupFilePath = path.join( - StorageCore.getBaseFolder(StorageFolder.BACKUPS), + StorageCore.getBaseFolder(StorageFolder.Backups), `immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, ); const databaseSemver = semver.coerce(databaseVersion); @@ -100,7 +100,7 @@ export class BackupService extends BaseService { if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <18.0.0')) { this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`); - return JobStatus.FAILED; + return JobStatus.Failed; } this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); @@ -179,6 +179,6 @@ export class BackupService extends BaseService { this.logger.log(`Database Backup Success`); await this.cleanupDatabaseBackups(); - return JobStatus.SUCCESS; + return JobStatus.Success; } } diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 021a5240f6..674b885dc4 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { isAbsolute } from 'node:path'; import { SALT_ROUNDS } from 'src/constants'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { BaseService } from 'src/services/base.service'; @@ -67,6 +68,63 @@ export class CliService extends BaseService { await this.updateConfig(config); } + async getSampleFilePaths(): Promise { + const [assets, people, users] = await Promise.all([ + this.assetRepository.getFileSamples(), + this.personRepository.getFileSamples(), + this.userRepository.getFileSamples(), + ]); + + const paths = []; + + for (const person of people) { + paths.push(person.thumbnailPath); + } + + for (const user of users) { + paths.push(user.profileImagePath); + } + + for (const asset of assets) { + paths.push( + asset.originalPath, + asset.sidecarPath, + asset.encodedVideoPath, + ...asset.files.map((file) => file.path), + ); + } + + return paths.filter(Boolean) as string[]; + } + + async migrateFilePaths({ + oldValue, + newValue, + confirm, + }: { + oldValue: string; + newValue: string; + confirm: (data: { sourceFolder: string; targetFolder: string }) => Promise; + }): Promise { + let sourceFolder = oldValue; + if (sourceFolder.startsWith('./')) { + sourceFolder = sourceFolder.slice(2); + } + + const targetFolder = newValue; + if (!isAbsolute(targetFolder)) { + throw new Error('Target media location must be an absolute path'); + } + + if (!(await confirm({ sourceFolder, targetFolder }))) { + return false; + } + + await this.databaseRepository.migrateFilePaths(sourceFolder, targetFolder); + + return true; + } + cleanup() { return this.databaseRepository.shutdown(); } diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index 09b22dfd5e..b4022ee864 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -19,7 +19,7 @@ describe(DatabaseService.name, () => { ({ sut, mocks } = newTestService(DatabaseService)); extensionRange = '0.2.x'; - mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VECTORCHORD); + mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VectorChord); mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange); versionBelowRange = '0.1.0'; @@ -28,7 +28,7 @@ describe(DatabaseService.name, () => { versionAboveRange = '0.3.0'; mocks.database.getExtensionVersions.mockResolvedValue([ { - name: DatabaseExtension.VECTORCHORD, + name: DatabaseExtension.VectorChord, installedVersion: null, availableVersion: minVersionInRange, }, @@ -49,9 +49,9 @@ describe(DatabaseService.name, () => { }); describe.each(>[ - { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, - { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, - { extension: DatabaseExtension.VECTORCHORD, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORCHORD] }, + { extension: DatabaseExtension.Vector, extensionName: EXTENSION_NAMES[DatabaseExtension.Vector] }, + { extension: DatabaseExtension.Vectors, extensionName: EXTENSION_NAMES[DatabaseExtension.Vectors] }, + { extension: DatabaseExtension.VectorChord, extensionName: EXTENSION_NAMES[DatabaseExtension.VectorChord] }, ])('should work with $extensionName', ({ extension, extensionName }) => { beforeEach(() => { mocks.database.getExtensionVersions.mockResolvedValue([ @@ -292,8 +292,8 @@ describe(DatabaseService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ - VectorIndex.CLIP, - VectorIndex.FACE, + VectorIndex.Clip, + VectorIndex.Face, ]); expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledTimes(1); expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); @@ -306,8 +306,8 @@ describe(DatabaseService.name, () => { await expect(sut.onBootstrap()).rejects.toBeDefined(); expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ - VectorIndex.CLIP, - VectorIndex.FACE, + VectorIndex.Clip, + VectorIndex.Face, ]); expect(mocks.database.runMigrations).not.toHaveBeenCalled(); expect(mocks.logger.fatal).not.toHaveBeenCalled(); @@ -330,7 +330,7 @@ describe(DatabaseService.name, () => { database: 'immich', }, skipMigrations: true, - vectorExtension: DatabaseExtension.VECTORS, + vectorExtension: DatabaseExtension.Vectors, }, }), ); @@ -356,12 +356,12 @@ describe(DatabaseService.name, () => { it(`should drop unused extension`, async () => { mocks.database.getExtensionVersions.mockResolvedValue([ { - name: DatabaseExtension.VECTORS, + name: DatabaseExtension.Vectors, installedVersion: minVersionInRange, availableVersion: minVersionInRange, }, { - name: DatabaseExtension.VECTORCHORD, + name: DatabaseExtension.VectorChord, installedVersion: null, availableVersion: minVersionInRange, }, @@ -369,19 +369,19 @@ describe(DatabaseService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); - expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); + expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VectorChord); + expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.Vectors); }); it(`should warn if unused extension could not be dropped`, async () => { mocks.database.getExtensionVersions.mockResolvedValue([ { - name: DatabaseExtension.VECTORS, + name: DatabaseExtension.Vectors, installedVersion: minVersionInRange, availableVersion: minVersionInRange, }, { - name: DatabaseExtension.VECTORCHORD, + name: DatabaseExtension.VectorChord, installedVersion: null, availableVersion: minVersionInRange, }, @@ -390,8 +390,8 @@ describe(DatabaseService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); - expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); + expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VectorChord); + expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.Vectors); expect(mocks.logger.warn).toHaveBeenCalledTimes(1); expect(mocks.logger.warn.mock.calls[0][0]).toContain('DROP EXTENSION vectors'); }); @@ -399,12 +399,12 @@ describe(DatabaseService.name, () => { it(`should not try to drop pgvector when using vectorchord`, async () => { mocks.database.getExtensionVersions.mockResolvedValue([ { - name: DatabaseExtension.VECTOR, + name: DatabaseExtension.Vector, installedVersion: minVersionInRange, availableVersion: minVersionInRange, }, { - name: DatabaseExtension.VECTORCHORD, + name: DatabaseExtension.VectorChord, installedVersion: minVersionInRange, availableVersion: minVersionInRange, }, diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index fd59e3aa67..e54be28fc2 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -100,7 +100,7 @@ export class DatabaseService extends BaseService { } try { - await this.databaseRepository.reindexVectorsIfNeeded([VectorIndex.CLIP, VectorIndex.FACE]); + await this.databaseRepository.reindexVectorsIfNeeded([VectorIndex.Clip, VectorIndex.Face]); } catch (error) { this.logger.warn( 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance. If you are upgrading directly from a version below 1.107.2, please upgrade to 1.107.2 first.', @@ -109,7 +109,7 @@ export class DatabaseService extends BaseService { } for (const { name: dbName, installedVersion } of extensionVersions) { - const isDepended = dbName === DatabaseExtension.VECTOR && extension === DatabaseExtension.VECTORCHORD; + const isDepended = dbName === DatabaseExtension.Vector && extension === DatabaseExtension.VectorChord; if (dbName !== extension && installedVersion && !isDepended) { await this.dropExtension(dbName); } @@ -120,8 +120,8 @@ export class DatabaseService extends BaseService { await this.databaseRepository.runMigrations(); } await Promise.all([ - this.databaseRepository.prewarm(VectorIndex.CLIP), - this.databaseRepository.prewarm(VectorIndex.FACE), + this.databaseRepository.prewarm(VectorIndex.Clip), + this.databaseRepository.prewarm(VectorIndex.Face), ]); }); } diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 7646637093..940767ff67 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -1,4 +1,5 @@ import { BadRequestException } from '@nestjs/common'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -46,7 +47,11 @@ describe(DownloadService.name, () => { }); expect(archiveMock.addFile).toHaveBeenCalledTimes(1); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('upload/library/IMG_123.jpg'), + 'IMG_123.jpg', + ); }); it('should log a warning if the original path could not be resolved', async () => { @@ -279,9 +284,15 @@ describe(DownloadService.name, () => { mocks.downloadRepository.downloadAssetIds.mockReturnValue( makeStream([{ id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }]), ); + mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue( makeStream([ - { id: 'asset-2', livePhotoVideoId: null, size: 23_456, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, + { + id: 'asset-2', + livePhotoVideoId: null, + size: 23_456, + originalPath: APP_MEDIA_LOCATION + '/encoded-video/uuid-MP.mp4', + }, ]), ); diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 02711b9bfd..a5f734e59c 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -17,15 +17,15 @@ export class DownloadService extends BaseService { if (dto.assetIds) { const assetIds = dto.assetIds; - await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); + await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: assetIds }); assets = this.downloadRepository.downloadAssetIds(assetIds); } else if (dto.albumId) { const albumId = dto.albumId; - await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); + await this.requireAccess({ auth, permission: Permission.AlbumDownload, ids: [albumId] }); assets = this.downloadRepository.downloadAlbumId(albumId); } else if (dto.userId) { const userId = dto.userId; - await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); + await this.requireAccess({ auth, permission: Permission.TimelineDownload, ids: [userId] }); assets = this.downloadRepository.downloadUserId(userId); } else { throw new BadRequestException('assetIds, albumId, or userId is required'); @@ -81,7 +81,7 @@ export class DownloadService extends BaseService { } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); + await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index b7d6d5fc96..e5ac9f82ba 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -12,10 +12,10 @@ const hasEmbedding = { id: 'asset-1', ownerId: 'user-id', stackId: null, - type: AssetType.IMAGE, + type: AssetType.Image, duplicateId: null, embedding: '[1, 2, 3, 4]', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }; const hasDupe = { @@ -78,7 +78,7 @@ describe(SearchService.name, () => { }, }); - await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -94,7 +94,7 @@ describe(SearchService.name, () => { }, }); - await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -108,7 +108,7 @@ describe(SearchService.name, () => { expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.DUPLICATE_DETECTION, + name: JobName.AssetDetectDuplicates, data: { id: assetStub.image.id }, }, ]); @@ -122,7 +122,7 @@ describe(SearchService.name, () => { expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.DUPLICATE_DETECTION, + name: JobName.AssetDetectDuplicates, data: { id: assetStub.image.id }, }, ]); @@ -154,7 +154,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id }); - expect(result).toBe(JobStatus.SKIPPED); + expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); }); @@ -171,7 +171,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id }); - expect(result).toBe(JobStatus.SKIPPED); + expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); }); @@ -180,7 +180,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); - expect(result).toBe(JobStatus.FAILED); + expect(result).toBe(JobStatus.Failed); expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); }); @@ -190,7 +190,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id }); - expect(result).toBe(JobStatus.SKIPPED); + expect(result).toBe(JobStatus.Skipped); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); }); @@ -198,12 +198,12 @@ describe(SearchService.name, () => { const id = assetStub.livePhotoMotionAsset.id; mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); const result = await sut.handleSearchDuplicates({ id }); - expect(result).toBe(JobStatus.SKIPPED); + expect(result).toBe(JobStatus.Skipped); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); }); @@ -212,7 +212,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); - expect(result).toBe(JobStatus.FAILED); + expect(result).toBe(JobStatus.Failed); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`); }); @@ -226,7 +226,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); - expect(result).toBe(JobStatus.SUCCESS); + expect(result).toBe(JobStatus.Success); expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({ assetId: hasEmbedding.id, embedding: hasEmbedding.embedding, @@ -253,7 +253,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); - expect(result).toBe(JobStatus.SUCCESS); + expect(result).toBe(JobStatus.Success); expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({ assetId: hasEmbedding.id, embedding: hasEmbedding.embedding, @@ -277,7 +277,7 @@ describe(SearchService.name, () => { const result = await sut.handleSearchDuplicates({ id: hasDupe.id }); - expect(result).toBe(JobStatus.SUCCESS); + expect(result).toBe(JobStatus.Success); expect(mocks.asset.update).toHaveBeenCalledWith({ id: hasDupe.id, duplicateId: null }); expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({ assetId: hasDupe.id, diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 99674d4c36..618754ff74 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -29,11 +29,11 @@ export class DuplicateService extends BaseService { await this.duplicateRepository.deleteAll(auth.user.id, dto.ids); } - @OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION }) - async handleQueueSearchDuplicates({ force }: JobOf): Promise { + @OnJob({ name: JobName.AssetDetectDuplicatesQueueAll, queue: QueueName.DuplicateDetection }) + async handleQueueSearchDuplicates({ force }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: false }); if (!isDuplicateDetectionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } let jobs: JobItem[] = []; @@ -44,7 +44,7 @@ export class DuplicateService extends BaseService { const assets = this.assetJobRepository.streamForSearchDuplicates(force); for await (const asset of assets) { - jobs.push({ name: JobName.DUPLICATE_DETECTION, data: { id: asset.id } }); + jobs.push({ name: JobName.AssetDetectDuplicates, data: { id: asset.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -52,40 +52,40 @@ export class DuplicateService extends BaseService { await queueAll(); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION }) - async handleSearchDuplicates({ id }: JobOf): Promise { + @OnJob({ name: JobName.AssetDetectDuplicates, queue: QueueName.DuplicateDetection }) + async handleSearchDuplicates({ id }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: true }); if (!isDuplicateDetectionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const asset = await this.assetJobRepository.getForSearchDuplicatesJob(id); if (!asset) { this.logger.error(`Asset ${id} not found`); - return JobStatus.FAILED; + return JobStatus.Failed; } if (asset.stackId) { this.logger.debug(`Asset ${id} is part of a stack, skipping`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } - if (asset.visibility === AssetVisibility.HIDDEN) { + if (asset.visibility === AssetVisibility.Hidden) { this.logger.debug(`Asset ${id} is not visible, skipping`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } - if (asset.visibility === AssetVisibility.LOCKED) { + if (asset.visibility === AssetVisibility.Locked) { this.logger.debug(`Asset ${id} is locked, skipping`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } if (!asset.embedding) { this.logger.debug(`Asset ${id} is missing embedding`); - return JobStatus.FAILED; + return JobStatus.Failed; } const duplicateAssets = await this.duplicateRepository.search({ @@ -110,7 +110,7 @@ export class DuplicateService extends BaseService { const duplicatesDetectedAt = new Date(); await this.assetRepository.upsertJobStatus(...assetIds.map((assetId) => ({ assetId, duplicatesDetectedAt }))); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async updateDuplicates( diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index a18eccdd8b..a57db736af 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -13,7 +13,7 @@ describe(JobService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(JobService, {})); - mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); it('should work', () => { @@ -25,10 +25,10 @@ describe(JobService.name, () => { sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig }); expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(15); - expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1); - expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DUPLICATE_DETECTION, 1); - expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BACKGROUND_TASK, 5); - expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.STORAGE_TEMPLATE_MIGRATION, 1); + expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1); + expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1); + expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5); + expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1); }); }); @@ -37,16 +37,16 @@ describe(JobService.name, () => { await sut.handleNightlyJobs(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION_CHECK }, - { name: JobName.USER_DELETE_CHECK }, - { name: JobName.PERSON_CLEANUP }, - { name: JobName.MEMORIES_CLEANUP }, - { name: JobName.CLEAN_OLD_SESSION_TOKENS }, - { name: JobName.CLEAN_OLD_AUDIT_LOGS }, - { name: JobName.MEMORIES_CREATE }, - { name: JobName.USER_SYNC_USAGE }, - { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, + { name: JobName.AssetDeleteCheck }, + { name: JobName.UserDeleteCheck }, + { name: JobName.PersonCleanup }, + { name: JobName.MemoryCleanup }, + { name: JobName.SessionCleanup }, + { name: JobName.AuditLogCleanup }, + { name: JobName.MemoryGenerate }, + { name: JobName.UserSyncUsage }, + { name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } }, + { name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } }, ]); }); }); @@ -82,49 +82,49 @@ describe(JobService.name, () => { }; await expect(sut.getAllJobsStatus()).resolves.toEqual({ - [QueueName.BACKGROUND_TASK]: expectedJobStatus, - [QueueName.DUPLICATE_DETECTION]: expectedJobStatus, - [QueueName.SMART_SEARCH]: expectedJobStatus, - [QueueName.METADATA_EXTRACTION]: expectedJobStatus, - [QueueName.SEARCH]: expectedJobStatus, - [QueueName.STORAGE_TEMPLATE_MIGRATION]: expectedJobStatus, - [QueueName.MIGRATION]: expectedJobStatus, - [QueueName.THUMBNAIL_GENERATION]: expectedJobStatus, - [QueueName.VIDEO_CONVERSION]: expectedJobStatus, - [QueueName.FACE_DETECTION]: expectedJobStatus, - [QueueName.FACIAL_RECOGNITION]: expectedJobStatus, - [QueueName.SIDECAR]: expectedJobStatus, - [QueueName.LIBRARY]: expectedJobStatus, - [QueueName.NOTIFICATION]: expectedJobStatus, - [QueueName.BACKUP_DATABASE]: expectedJobStatus, + [QueueName.BackgroundTask]: expectedJobStatus, + [QueueName.DuplicateDetection]: expectedJobStatus, + [QueueName.SmartSearch]: expectedJobStatus, + [QueueName.MetadataExtraction]: expectedJobStatus, + [QueueName.Search]: expectedJobStatus, + [QueueName.StorageTemplateMigration]: expectedJobStatus, + [QueueName.Migration]: expectedJobStatus, + [QueueName.ThumbnailGeneration]: expectedJobStatus, + [QueueName.VideoConversion]: expectedJobStatus, + [QueueName.FaceDetection]: expectedJobStatus, + [QueueName.FacialRecognition]: expectedJobStatus, + [QueueName.Sidecar]: expectedJobStatus, + [QueueName.Library]: expectedJobStatus, + [QueueName.Notification]: expectedJobStatus, + [QueueName.BackupDatabase]: expectedJobStatus, }); }); }); describe('handleCommand', () => { it('should handle a pause command', async () => { - await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.PAUSE, force: false }); + await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Pause, force: false }); - expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); + expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should handle a resume command', async () => { - await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.RESUME, force: false }); + await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Resume, force: false }); - expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); + expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should handle an empty command', async () => { - await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.EMPTY, force: false }); + await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Empty, force: false }); - expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); + expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should not start a job that is already running', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false }); await expect( - sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }), + sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); @@ -134,80 +134,86 @@ describe(JobService.name, () => { it('should handle a start video conversion command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } }); }); it('should handle a start storage template migration command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.STORAGE_TEMPLATE_MIGRATION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.StorageTemplateMigration, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration }); }); it('should handle a start smart search command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.SMART_SEARCH, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.SmartSearch, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SMART_SEARCH, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } }); }); it('should handle a start metadata extraction command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.AssetExtractMetadataQueueAll, + data: { force: false }, + }); }); it('should handle a start sidecar command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.SIDECAR, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.Sidecar, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SIDECAR, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } }); }); it('should handle a start thumbnail generation command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.THUMBNAIL_GENERATION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.ThumbnailGeneration, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.AssetGenerateThumbnailsQueueAll, + data: { force: false }, + }); }); it('should handle a start face detection command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.FACE_DETECTION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.FaceDetection, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACE_DETECTION, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } }); }); it('should handle a start facial recognition command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.FACIAL_RECOGNITION, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.FacialRecognition, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } }); }); it('should handle a start backup database command', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.handleCommand(QueueName.BACKUP_DATABASE, { command: JobCommand.START, force: false }); + await sut.handleCommand(QueueName.BackupDatabase, { command: JobCommand.Start, force: false }); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.BACKUP_DATABASE, data: { force: false } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } }); }); it('should throw a bad request when an invalid queue is used', async () => { mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await expect( - sut.handleCommand(QueueName.BACKGROUND_TASK, { command: JobCommand.START, force: false }), + sut.handleCommand(QueueName.BackgroundTask, { command: JobCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); @@ -217,70 +223,70 @@ describe(JobService.name, () => { describe('onJobStart', () => { it('should process a successful job', async () => { - mocks.job.run.mockResolvedValue(JobStatus.SUCCESS); + mocks.job.run.mockResolvedValue(JobStatus.Success); - await sut.onJobStart(QueueName.BACKGROUND_TASK, { - name: JobName.DELETE_FILES, + await sut.onJobStart(QueueName.BackgroundTask, { + name: JobName.FileDelete, data: { files: ['path/to/file'] }, }); expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1); expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1); - expect(mocks.telemetry.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.delete_files.success', 1); + expect(mocks.telemetry.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.file_delete.success', 1); expect(mocks.logger.error).not.toHaveBeenCalled(); }); const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [ { - item: { name: JobName.SIDECAR_SYNC, data: { id: 'asset-1' } }, - jobs: [JobName.METADATA_EXTRACTION], + item: { name: JobName.SidecarSync, data: { id: 'asset-1' } }, + jobs: [JobName.AssetExtractMetadata], }, { - item: { name: JobName.SIDECAR_DISCOVERY, data: { id: 'asset-1' } }, - jobs: [JobName.METADATA_EXTRACTION], + item: { name: JobName.SidecarDiscovery, data: { id: 'asset-1' } }, + jobs: [JobName.AssetExtractMetadata], }, { - item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.GENERATE_THUMBNAILS], + item: { name: JobName.StorageTemplateMigrationSingle, data: { id: 'asset-1', source: 'upload' } }, + jobs: [JobName.AssetGenerateThumbnails], }, { - item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, + item: { name: JobName.StorageTemplateMigrationSingle, data: { id: 'asset-1' } }, jobs: [], }, { - item: { name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'asset-1' } }, + item: { name: JobName.PersonGenerateThumbnail, data: { id: 'asset-1' } }, jobs: [], }, { - item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, + item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], stub: [assetStub.image], }, { - item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, + item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], stub: [assetStub.video], }, { - item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION], + item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, + jobs: [JobName.SmartSearch, JobName.AssetDetectFaces], stub: [assetStub.livePhotoStillAsset], }, { - item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], + item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, + jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.AssetEncodeVideo], stub: [assetStub.video], }, { - item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } }, + item: { name: JobName.SmartSearch, data: { id: 'asset-1' } }, jobs: [], }, { - item: { name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }, + item: { name: JobName.AssetDetectFaces, data: { id: 'asset-1' } }, jobs: [], }, { - item: { name: JobName.FACIAL_RECOGNITION, data: { id: 'asset-1' } }, + item: { name: JobName.FacialRecognition, data: { id: 'asset-1' } }, jobs: [], }, ]; @@ -291,9 +297,9 @@ describe(JobService.name, () => { mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue(stub); } - mocks.job.run.mockResolvedValue(JobStatus.SUCCESS); + mocks.job.run.mockResolvedValue(JobStatus.Success); - await sut.onJobStart(QueueName.BACKGROUND_TASK, item); + await sut.onJobStart(QueueName.BackgroundTask, item); if (jobs.length > 1) { expect(mocks.job.queueAll).toHaveBeenCalledWith( @@ -308,9 +314,9 @@ describe(JobService.name, () => { }); it(`should not queue any jobs when ${item.name} fails`, async () => { - mocks.job.run.mockResolvedValue(JobStatus.FAILED); + mocks.job.run.mockResolvedValue(JobStatus.Failed); - await sut.onJobStart(QueueName.BACKGROUND_TASK, item); + await sut.onJobStart(QueueName.BackgroundTask, item); expect(mocks.job.queueAll).not.toHaveBeenCalled(); }); diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index f0bbefc8f0..c67f3af39f 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -27,28 +27,28 @@ import { handlePromiseError } from 'src/utils/misc'; const asJobItem = (dto: JobCreateDto): JobItem => { switch (dto.name) { - case ManualJobName.TAG_CLEANUP: { - return { name: JobName.TAG_CLEANUP }; + case ManualJobName.TagCleanup: { + return { name: JobName.TagCleanup }; } - case ManualJobName.PERSON_CLEANUP: { - return { name: JobName.PERSON_CLEANUP }; + case ManualJobName.PersonCleanup: { + return { name: JobName.PersonCleanup }; } - case ManualJobName.USER_CLEANUP: { - return { name: JobName.USER_DELETE_CHECK }; + case ManualJobName.UserCleanup: { + return { name: JobName.UserDeleteCheck }; } - case ManualJobName.MEMORY_CLEANUP: { - return { name: JobName.MEMORIES_CLEANUP }; + case ManualJobName.MemoryCleanup: { + return { name: JobName.MemoryCleanup }; } - case ManualJobName.MEMORY_CREATE: { - return { name: JobName.MEMORIES_CREATE }; + case ManualJobName.MemoryCreate: { + return { name: JobName.MemoryGenerate }; } - case ManualJobName.BACKUP_DATABASE: { - return { name: JobName.BACKUP_DATABASE }; + case ManualJobName.BackupDatabase: { + return { name: JobName.DatabaseBackup }; } default: { @@ -69,7 +69,7 @@ export class JobService extends BaseService { @OnEvent({ name: 'ConfigInit' }) async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) { - if (this.worker === ImmichWorker.MICROSERVICES) { + if (this.worker === ImmichWorker.Microservices) { this.updateQueueConcurrency(config); return; } @@ -89,7 +89,7 @@ export class JobService extends BaseService { @OnEvent({ name: 'ConfigUpdate', server: true }) onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) { - if (this.worker === ImmichWorker.MICROSERVICES) { + if (this.worker === ImmichWorker.Microservices) { this.updateQueueConcurrency(config); return; } @@ -104,7 +104,7 @@ export class JobService extends BaseService { @OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService }) onBootstrap() { this.jobRepository.setup(this.services); - if (this.worker === ImmichWorker.MICROSERVICES) { + if (this.worker === ImmichWorker.Microservices) { this.jobRepository.startWorkers(); } } @@ -133,28 +133,28 @@ export class JobService extends BaseService { this.logger.debug(`Handling command: queue=${queueName},command=${dto.command},force=${dto.force}`); switch (dto.command) { - case JobCommand.START: { + case JobCommand.Start: { await this.start(queueName, dto); break; } - case JobCommand.PAUSE: { + case JobCommand.Pause: { await this.jobRepository.pause(queueName); break; } - case JobCommand.RESUME: { + case JobCommand.Resume: { await this.jobRepository.resume(queueName); break; } - case JobCommand.EMPTY: { + case JobCommand.Empty: { await this.jobRepository.empty(queueName); break; } - case JobCommand.CLEAR_FAILED: { - const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED); + case JobCommand.ClearFailed: { + const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.Failed); this.logger.debug(`Cleared failed jobs: ${failedJobs}`); break; } @@ -189,52 +189,52 @@ export class JobService extends BaseService { this.telemetryRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1); switch (name) { - case QueueName.VIDEO_CONVERSION: { - return this.jobRepository.queue({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force } }); + case QueueName.VideoConversion: { + return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } }); } - case QueueName.STORAGE_TEMPLATE_MIGRATION: { - return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); + case QueueName.StorageTemplateMigration: { + return this.jobRepository.queue({ name: JobName.StorageTemplateMigration }); } - case QueueName.MIGRATION: { - return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION }); + case QueueName.Migration: { + return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll }); } - case QueueName.SMART_SEARCH: { - return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } }); + case QueueName.SmartSearch: { + return this.jobRepository.queue({ name: JobName.SmartSearchQueueAll, data: { force } }); } - case QueueName.DUPLICATE_DETECTION: { - return this.jobRepository.queue({ name: JobName.QUEUE_DUPLICATE_DETECTION, data: { force } }); + case QueueName.DuplicateDetection: { + return this.jobRepository.queue({ name: JobName.AssetDetectDuplicatesQueueAll, data: { force } }); } - case QueueName.METADATA_EXTRACTION: { - return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); + case QueueName.MetadataExtraction: { + return this.jobRepository.queue({ name: JobName.AssetExtractMetadataQueueAll, data: { force } }); } - case QueueName.SIDECAR: { - return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); + case QueueName.Sidecar: { + return this.jobRepository.queue({ name: JobName.SidecarQueueAll, data: { force } }); } - case QueueName.THUMBNAIL_GENERATION: { - return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); + case QueueName.ThumbnailGeneration: { + return this.jobRepository.queue({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force } }); } - case QueueName.FACE_DETECTION: { - return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } }); + case QueueName.FaceDetection: { + return this.jobRepository.queue({ name: JobName.AssetDetectFacesQueueAll, data: { force } }); } - case QueueName.FACIAL_RECOGNITION: { - return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } }); + case QueueName.FacialRecognition: { + return this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force } }); } - case QueueName.LIBRARY: { - return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } }); + case QueueName.Library: { + return this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: { force } }); } - case QueueName.BACKUP_DATABASE: { - return this.jobRepository.queue({ name: JobName.BACKUP_DATABASE, data: { force } }); + case QueueName.BackupDatabase: { + return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } }); } default: { @@ -249,9 +249,9 @@ export class JobService extends BaseService { this.telemetryRepository.jobs.addToGauge(queueMetric, 1); try { const status = await this.jobRepository.run(job); - const jobMetric = `immich.jobs.${job.name.replaceAll('-', '_')}.${status}`; + const jobMetric = `immich.jobs.${snakeCase(job.name)}.${status}`; this.telemetryRepository.jobs.addToCounter(jobMetric, 1); - if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) { + if (status === JobStatus.Success || status == JobStatus.Skipped) { await this.onDone(job); } } catch (error: Error | any) { @@ -263,10 +263,10 @@ export class JobService extends BaseService { private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName { return ![ - QueueName.FACIAL_RECOGNITION, - QueueName.STORAGE_TEMPLATE_MIGRATION, - QueueName.DUPLICATE_DETECTION, - QueueName.BACKUP_DATABASE, + QueueName.FacialRecognition, + QueueName.StorageTemplateMigration, + QueueName.DuplicateDetection, + QueueName.BackupDatabase, ].includes(name); } @@ -276,29 +276,29 @@ export class JobService extends BaseService { if (config.nightlyTasks.databaseCleanup) { jobs.push( - { name: JobName.ASSET_DELETION_CHECK }, - { name: JobName.USER_DELETE_CHECK }, - { name: JobName.PERSON_CLEANUP }, - { name: JobName.MEMORIES_CLEANUP }, - { name: JobName.CLEAN_OLD_SESSION_TOKENS }, - { name: JobName.CLEAN_OLD_AUDIT_LOGS }, + { name: JobName.AssetDeleteCheck }, + { name: JobName.UserDeleteCheck }, + { name: JobName.PersonCleanup }, + { name: JobName.MemoryCleanup }, + { name: JobName.SessionCleanup }, + { name: JobName.AuditLogCleanup }, ); } if (config.nightlyTasks.generateMemories) { - jobs.push({ name: JobName.MEMORIES_CREATE }); + jobs.push({ name: JobName.MemoryGenerate }); } if (config.nightlyTasks.syncQuotaUsage) { - jobs.push({ name: JobName.USER_SYNC_USAGE }); + jobs.push({ name: JobName.UserSyncUsage }); } if (config.nightlyTasks.missingThumbnails) { - jobs.push({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); + jobs.push({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } }); } if (config.nightlyTasks.clusterNewFaces) { - jobs.push({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }); + jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } }); } await this.jobRepository.queueAll(jobs); @@ -309,28 +309,28 @@ export class JobService extends BaseService { */ private async onDone(item: JobItem) { switch (item.name) { - case JobName.SIDECAR_SYNC: - case JobName.SIDECAR_DISCOVERY: { - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data }); + case JobName.SidecarSync: + case JobName.SidecarDiscovery: { + await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: item.data }); break; } - case JobName.SIDECAR_WRITE: { + case JobName.SidecarWrite: { await this.jobRepository.queue({ - name: JobName.METADATA_EXTRACTION, + name: JobName.AssetExtractMetadata, data: { id: item.data.id, source: 'sidecar-write' }, }); break; } - case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { + case JobName.StorageTemplateMigrationSingle: { if (item.data.source === 'upload' || item.data.source === 'copy') { - await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: item.data }); + await this.jobRepository.queue({ name: JobName.AssetGenerateThumbnails, data: item.data }); } break; } - case JobName.GENERATE_PERSON_THUMBNAIL: { + case JobName.PersonGenerateThumbnail: { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { @@ -339,7 +339,7 @@ export class JobService extends BaseService { break; } - case JobName.GENERATE_THUMBNAILS: { + case JobName.AssetGenerateThumbnails: { if (!item.data.notify && item.data.source !== 'upload') { break; } @@ -351,16 +351,16 @@ export class JobService extends BaseService { } const jobs: JobItem[] = [ - { name: JobName.SMART_SEARCH, data: item.data }, - { name: JobName.FACE_DETECTION, data: item.data }, + { name: JobName.SmartSearch, data: item.data }, + { name: JobName.AssetDetectFaces, data: item.data }, ]; - if (asset.type === AssetType.VIDEO) { - jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data }); + if (asset.type === AssetType.Video) { + jobs.push({ name: JobName.AssetEncodeVideo, data: item.data }); } await this.jobRepository.queueAll(jobs); - if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) { + if (asset.visibility === AssetVisibility.Timeline || asset.visibility === AssetVisibility.Archive) { this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset)); if (asset.exifInfo) { const exif = asset.exifInfo; @@ -417,14 +417,14 @@ export class JobService extends BaseService { break; } - case JobName.SMART_SEARCH: { + case JobName.SmartSearch: { if (item.data.source === 'upload') { - await this.jobRepository.queue({ name: JobName.DUPLICATE_DETECTION, data: item.data }); + await this.jobRepository.queue({ name: JobName.AssetDetectDuplicates, data: item.data }); } break; } - case JobName.USER_DELETION: { + case JobName.UserDelete: { this.eventRepository.clientBroadcast('on_user_delete', item.data.id); break; } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index ab69e22b99..308c80fb37 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; -import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; +import { APP_MEDIA_LOCATION, JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; @@ -27,7 +27,7 @@ describe(LibraryService.name, () => { ({ sut, mocks } = newTestService(LibraryService, {})); mocks.database.tryLock.mockResolvedValue(true); - mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); it('should work', () => { @@ -173,7 +173,7 @@ describe(LibraryService.name, () => { await sut.handleQueueSyncFiles({ id: library.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths: ['/data/user1/photo.jpg'], @@ -185,7 +185,7 @@ describe(LibraryService.name, () => { it('should fail when library is not found', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); - await expect(sut.handleQueueSyncFiles({ id: library.id })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueSyncFiles({ id: library.id })).resolves.toBe(JobStatus.Skipped); }); it('should ignore import paths that do not exist', async () => { @@ -228,7 +228,7 @@ describe(LibraryService.name, () => { await sut.handleQueueSyncFiles({ id: library.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths: ['/data/user1/photo.jpg'], @@ -240,7 +240,7 @@ describe(LibraryService.name, () => { it("should fail when library can't be found", async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); - await expect(sut.handleQueueSyncFiles({ id: library.id })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueSyncFiles({ id: library.id })).resolves.toBe(JobStatus.Skipped); }); it('should ignore import paths that do not exist', async () => { @@ -282,7 +282,7 @@ describe(LibraryService.name, () => { const response = await sut.handleQueueSyncAssets({ id: library.id }); - expect(response).toBe(JobStatus.SUCCESS); + expect(response).toBe(JobStatus.Success); expect(mocks.asset.detectOfflineExternalAssets).toHaveBeenCalledWith( library.id, library.importPaths, @@ -300,7 +300,7 @@ describe(LibraryService.name, () => { const response = await sut.handleQueueSyncAssets({ id: library.id }); - expect(response).toBe(JobStatus.SUCCESS); + expect(response).toBe(JobStatus.Success); expect(mocks.asset.detectOfflineExternalAssets).not.toHaveBeenCalled(); }); @@ -317,7 +317,7 @@ describe(LibraryService.name, () => { const response = await sut.handleQueueSyncAssets({ id: library.id }); expect(mocks.job.queue).toBeCalledWith({ - name: JobName.LIBRARY_SYNC_ASSETS, + name: JobName.LibrarySyncAssets, data: { libraryId: library.id, importPaths: library.importPaths, @@ -328,7 +328,7 @@ describe(LibraryService.name, () => { }, }); - expect(response).toBe(JobStatus.SUCCESS); + expect(response).toBe(JobStatus.Success); expect(mocks.asset.detectOfflineExternalAssets).toHaveBeenCalledWith( library.id, library.importPaths, @@ -337,7 +337,7 @@ describe(LibraryService.name, () => { }); it("should fail if library can't be found", async () => { - await expect(sut.handleQueueSyncAssets({ id: newUuid() })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueSyncAssets({ id: newUuid() })).resolves.toBe(JobStatus.Skipped); }); }); @@ -355,7 +355,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { isOffline: true, @@ -376,7 +376,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { isOffline: true, @@ -397,7 +397,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); }); @@ -415,7 +415,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { isOffline: false, @@ -436,7 +436,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); @@ -456,7 +456,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); @@ -476,7 +476,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); }); @@ -494,7 +494,7 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith( [assetStub.trashedOffline.id], @@ -523,11 +523,11 @@ describe(LibraryService.name, () => { mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockResolvedValue({ mtime } as Stats); - await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SIDECAR_DISCOVERY, + name: JobName.SidecarDiscovery, data: { id: assetStub.external.id, source: 'upload', @@ -557,7 +557,7 @@ describe(LibraryService.name, () => { mocks.asset.createAll.mockResolvedValue([assetStub.image]); mocks.library.get.mockResolvedValue(library); - await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.createAll).toHaveBeenCalledWith([ expect.objectContaining({ @@ -565,7 +565,7 @@ describe(LibraryService.name, () => { libraryId: library.id, originalPath: '/data/user1/photo.jpg', deviceId: 'Library Import', - type: AssetType.IMAGE, + type: AssetType.Image, originalFileName: 'photo.jpg', isExternal: true, }), @@ -573,7 +573,7 @@ describe(LibraryService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SIDECAR_DISCOVERY, + name: JobName.SidecarDiscovery, data: { id: assetStub.image.id, source: 'upload', @@ -592,7 +592,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); - await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.Failed); expect(mocks.asset.createAll.mock.calls).toEqual([]); }); @@ -607,7 +607,7 @@ describe(LibraryService.name, () => { await sut.delete(library.id); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.LIBRARY_DELETE, data: { id: library.id } }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.LibraryDelete, data: { id: library.id } }); expect(mocks.library.softDelete).toHaveBeenCalledWith(library.id); }); @@ -620,7 +620,7 @@ describe(LibraryService.name, () => { await sut.delete(library.id); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_DELETE, + name: JobName.LibraryDelete, data: { id: library.id }, }); @@ -838,11 +838,11 @@ describe(LibraryService.name, () => { const library2 = factory.library({ deletedAt: new Date() }); mocks.library.getAllDeleted.mockResolvedValue([library1, library2]); - await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.Success); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.LIBRARY_DELETE, data: { id: library1.id } }, - { name: JobName.LIBRARY_DELETE, data: { id: library2.id } }, + { name: JobName.LibraryDelete, data: { id: library1.id } }, + { name: JobName.LibraryDelete, data: { id: library2.id } }, ]); }); }); @@ -968,7 +968,7 @@ describe(LibraryService.name, () => { await sut.watchAll(); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths: ['/foo/photo.jpg'], @@ -989,7 +989,7 @@ describe(LibraryService.name, () => { await sut.watchAll(); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths: ['/foo/photo.jpg'], @@ -1010,7 +1010,7 @@ describe(LibraryService.name, () => { await sut.watchAll(); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_ASSET_REMOVAL, + name: JobName.LibraryRemoveAsset, data: { libraryId: library.id, paths: [assetStub.image.originalPath], @@ -1106,7 +1106,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.streamAssetIds.mockReturnValue(makeStream([])); - await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.Success); expect(mocks.library.delete).toHaveBeenCalled(); }); @@ -1117,7 +1117,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.image1])); - await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.Success); }); }); @@ -1131,11 +1131,11 @@ describe(LibraryService.name, () => { expect(mocks.job.queue).toHaveBeenCalledTimes(2); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_QUEUE_SYNC_FILES, + name: JobName.LibrarySyncFilesQueueAll, data: { id: library.id }, }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, + name: JobName.LibrarySyncAssetsQueueAll, data: { id: library.id }, }); }); @@ -1147,14 +1147,14 @@ describe(LibraryService.name, () => { mocks.library.getAll.mockResolvedValue([library]); - await expect(sut.handleQueueScanAll()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueScanAll()).resolves.toBe(JobStatus.Success); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_QUEUE_CLEANUP, + name: JobName.LibraryDeleteCheck, data: {}, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.LIBRARY_QUEUE_SYNC_FILES, data: { id: library.id } }, + { name: JobName.LibrarySyncFilesQueueAll, data: { id: library.id } }, ]); }); }); @@ -1264,7 +1264,7 @@ describe(LibraryService.name, () => { }); it('should detect when import path is in immich media folder', async () => { - const importPaths = ['upload/thumbs', `${process.cwd()}/xyz`, 'upload/library']; + const importPaths = [APP_MEDIA_LOCATION + '/thumbs', `${process.cwd()}/xyz`, APP_MEDIA_LOCATION + '/library']; const library = factory.library({ importPaths }); mocks.storage.stat.mockResolvedValue({ isDirectory: () => true } as Stats); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 911ea3b702..4c96ad0062 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -32,7 +32,7 @@ export class LibraryService extends BaseService { private lock = false; private watchers: Record Promise> = {}; - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] }) + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) async onConfigInit({ newConfig: { library: { watch, scan }, @@ -47,8 +47,7 @@ export class LibraryService extends BaseService { this.cronRepository.create({ name: CronJob.LibraryScan, expression: scan.cronExpression, - onTick: () => - handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger), + onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.LibraryScanQueueAll }), this.logger), start: scan.enabled, }); } @@ -103,7 +102,7 @@ export class LibraryService extends BaseService { if (matcher(path)) { this.logger.debug(`File ${event} event received for ${path} in library ${library.id}}`); await this.jobRepository.queue({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths: [path] }, }); } else { @@ -114,7 +113,7 @@ export class LibraryService extends BaseService { const deletionHandler = async (path: string) => { this.logger.debug(`File unlink event received for ${path} in library ${library.id}}`); await this.jobRepository.queue({ - name: JobName.LIBRARY_ASSET_REMOVAL, + name: JobName.LibraryRemoveAsset, data: { libraryId: library.id, paths: [path] }, }); }; @@ -199,7 +198,7 @@ export class LibraryService extends BaseService { return libraries.map((library) => mapLibrary(library)); } - @OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY }) + @OnJob({ name: JobName.LibraryDeleteCheck, queue: QueueName.Library }) async handleQueueCleanup(): Promise { this.logger.log('Checking for any libraries pending deletion...'); const pendingDeletions = await this.libraryRepository.getAllDeleted(); @@ -208,11 +207,11 @@ export class LibraryService extends BaseService { this.logger.log(`Found ${pendingDeletions.length} ${libraryString} pending deletion, cleaning up...`); await this.jobRepository.queueAll( - pendingDeletions.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), + pendingDeletions.map((libraryToDelete) => ({ name: JobName.LibraryDelete, data: { id: libraryToDelete.id } })), ); } - return JobStatus.SUCCESS; + return JobStatus.Success; } async create(dto: CreateLibraryDto): Promise { @@ -225,16 +224,16 @@ export class LibraryService extends BaseService { return mapLibrary(library); } - @OnJob({ name: JobName.LIBRARY_SYNC_FILES, queue: QueueName.LIBRARY }) - async handleSyncFiles(job: JobOf): Promise { + @OnJob({ name: JobName.LibrarySyncFiles, queue: QueueName.Library }) + async handleSyncFiles(job: JobOf): Promise { const library = await this.libraryRepository.get(job.libraryId); // We need to check if the library still exists as it could have been deleted after the scan was queued if (!library) { this.logger.debug(`Library ${job.libraryId} not found, skipping file import`); - return JobStatus.FAILED; + return JobStatus.Failed; } else if (library.deletedAt) { this.logger.debug(`Library ${job.libraryId} is deleted, won't import assets into it`); - return JobStatus.FAILED; + return JobStatus.Failed; } const assetImports: Insertable[] = []; @@ -263,7 +262,7 @@ export class LibraryService extends BaseService { await this.queuePostSyncJobs(assetIds); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async validateImportPath(importPath: string): Promise { @@ -339,11 +338,11 @@ export class LibraryService extends BaseService { } await this.libraryRepository.softDelete(id); - await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } }); + await this.jobRepository.queue({ name: JobName.LibraryDelete, data: { id } }); } - @OnJob({ name: JobName.LIBRARY_DELETE, queue: QueueName.LIBRARY }) - async handleDeleteLibrary(job: JobOf): Promise { + @OnJob({ name: JobName.LibraryDelete, queue: QueueName.Library }) + async handleDeleteLibrary(job: JobOf): Promise { const libraryId = job.id; await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() }); @@ -356,7 +355,7 @@ export class LibraryService extends BaseService { assetsFound = true; this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`); await this.jobRepository.queueAll( - chunk.map((id) => ({ name: JobName.ASSET_DELETION, data: { id, deleteOnDisk: false } })), + chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })), ); chunk = []; } @@ -379,7 +378,7 @@ export class LibraryService extends BaseService { await this.libraryRepository.delete(libraryId); } - return JobStatus.SUCCESS; + return JobStatus.Success; } private async processEntity(filePath: string, ownerId: string, libraryId: string) { @@ -398,7 +397,7 @@ export class LibraryService extends BaseService { // TODO: device asset id is deprecated, remove it deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''), deviceId: 'Library Import', - type: mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE, + type: mimeTypes.isVideo(assetPath) ? AssetType.Video : AssetType.Image, originalFileName: parse(assetPath).base, isExternal: true, livePhotoVideoId: null, @@ -411,7 +410,7 @@ export class LibraryService extends BaseService { // We queue a sidecar discovery which, in turn, queues metadata extraction await this.jobRepository.queueAll( assetIds.map((assetId) => ({ - name: JobName.SIDECAR_DISCOVERY, + name: JobName.SidecarDiscovery, data: { id: assetId, source: 'upload' }, })), ); @@ -423,30 +422,30 @@ export class LibraryService extends BaseService { this.logger.log(`Starting to scan library ${id}`); await this.jobRepository.queue({ - name: JobName.LIBRARY_QUEUE_SYNC_FILES, + name: JobName.LibrarySyncFilesQueueAll, data: { id, }, }); - await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } }); + await this.jobRepository.queue({ name: JobName.LibrarySyncAssetsQueueAll, data: { id } }); } async queueScanAll() { - await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: {} }); + await this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: {} }); } - @OnJob({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, queue: QueueName.LIBRARY }) + @OnJob({ name: JobName.LibraryScanQueueAll, queue: QueueName.Library }) async handleQueueScanAll(): Promise { this.logger.log(`Initiating scan of all external libraries...`); - await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); + await this.jobRepository.queue({ name: JobName.LibraryDeleteCheck, data: {} }); const libraries = await this.libraryRepository.getAll(true); await this.jobRepository.queueAll( libraries.map((library) => ({ - name: JobName.LIBRARY_QUEUE_SYNC_FILES, + name: JobName.LibrarySyncFilesQueueAll, data: { id: library.id, }, @@ -454,18 +453,18 @@ export class LibraryService extends BaseService { ); await this.jobRepository.queueAll( libraries.map((library) => ({ - name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, + name: JobName.LibrarySyncAssetsQueueAll, data: { id: library.id, }, })), ); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.LIBRARY_SYNC_ASSETS, queue: QueueName.LIBRARY }) - async handleSyncAssets(job: JobOf): Promise { + @OnJob({ name: JobName.LibrarySyncAssets, queue: QueueName.Library }) + async handleSyncAssets(job: JobOf): Promise { const assets = await this.assetJobRepository.getForSyncAssets(job.assetIds); const assetIdsToOffline: string[] = []; @@ -486,7 +485,7 @@ export class LibraryService extends BaseService { const action = this.checkExistingAsset(asset, stat); switch (action) { case AssetSyncResult.OFFLINE: { - if (asset.status === AssetStatus.TRASHED) { + if (asset.status === AssetStatus.Trashed) { trashedAssetIdsToOffline.push(asset.id); } else { assetIdsToOffline.push(asset.id); @@ -511,7 +510,7 @@ export class LibraryService extends BaseService { if (!isExcluded) { this.logger.debug(`Offline asset ${asset.originalPath} is now online in library ${job.libraryId}`); - if (asset.status === AssetStatus.TRASHED) { + if (asset.status === AssetStatus.Trashed) { trashedAssetIdsToOnline.push(asset.id); } else { assetIdsToOnline.push(asset.id); @@ -557,7 +556,7 @@ export class LibraryService extends BaseService { `Checked existing asset(s): ${assetIdsToOffline.length + trashedAssetIdsToOffline.length} offlined, ${assetIdsToOnline.length + trashedAssetIdsToOnline.length} onlined, ${assetIdsToUpdate.length} updated, ${remainingCount} unchanged of current batch of ${assets.length} (Total progress: ${job.progressCounter} of ${job.totalAssets}, ${cumulativePercentage} %) in library ${job.libraryId}.`, ); - return JobStatus.SUCCESS; + return JobStatus.Success; } private checkExistingAsset( @@ -585,7 +584,7 @@ export class LibraryService extends BaseService { return AssetSyncResult.OFFLINE; } - if (asset.isOffline && asset.status !== AssetStatus.DELETED) { + if (asset.isOffline && asset.status !== AssetStatus.Deleted) { // Only perform the expensive check if the asset is offline return AssetSyncResult.CHECK_OFFLINE; } @@ -599,12 +598,12 @@ export class LibraryService extends BaseService { return AssetSyncResult.DO_NOTHING; } - @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_FILES, queue: QueueName.LIBRARY }) - async handleQueueSyncFiles(job: JobOf): Promise { + @OnJob({ name: JobName.LibrarySyncFilesQueueAll, queue: QueueName.Library }) + async handleQueueSyncFiles(job: JobOf): Promise { const library = await this.libraryRepository.get(job.id); if (!library) { this.logger.debug(`Library ${job.id} not found, skipping refresh`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } this.logger.debug(`Validating import paths for library ${library.id}...`); @@ -623,7 +622,7 @@ export class LibraryService extends BaseService { if (validImportPaths.length === 0) { this.logger.warn(`No valid import paths found for library ${library.id}`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const pathsOnDisk = this.storageRepository.walk({ @@ -646,7 +645,7 @@ export class LibraryService extends BaseService { importCount += paths.length; await this.jobRepository.queue({ - name: JobName.LIBRARY_SYNC_FILES, + name: JobName.LibrarySyncFiles, data: { libraryId: library.id, paths, @@ -666,11 +665,11 @@ export class LibraryService extends BaseService { await this.libraryRepository.update(job.id, { refreshedAt: new Date() }); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.LIBRARY_ASSET_REMOVAL, queue: QueueName.LIBRARY }) - async handleAssetRemoval(job: JobOf): Promise { + @OnJob({ name: JobName.LibraryRemoveAsset, queue: QueueName.Library }) + async handleAssetRemoval(job: JobOf): Promise { // This is only for handling file unlink events via the file watcher this.logger.verbose(`Deleting asset(s) ${job.paths} from library ${job.libraryId}`); for (const assetPath of job.paths) { @@ -680,20 +679,20 @@ export class LibraryService extends BaseService { } } - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, queue: QueueName.LIBRARY }) - async handleQueueSyncAssets(job: JobOf): Promise { + @OnJob({ name: JobName.LibrarySyncAssetsQueueAll, queue: QueueName.Library }) + async handleQueueSyncAssets(job: JobOf): Promise { const library = await this.libraryRepository.get(job.id); if (!library) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const assetCount = await this.assetRepository.getLibraryAssetCount(job.id); if (!assetCount) { this.logger.log(`Library ${library.id} is empty, no need to check assets`); - return JobStatus.SUCCESS; + return JobStatus.Success; } this.logger.log( @@ -713,7 +712,7 @@ export class LibraryService extends BaseService { ); if (affectedAssetCount === assetCount) { - return JobStatus.SUCCESS; + return JobStatus.Success; } let chunk: string[] = []; @@ -724,7 +723,7 @@ export class LibraryService extends BaseService { count += chunk.length; await this.jobRepository.queue({ - name: JobName.LIBRARY_SYNC_ASSETS, + name: JobName.LibrarySyncAssets, data: { libraryId: library.id, importPaths: library.importPaths, @@ -758,7 +757,7 @@ export class LibraryService extends BaseService { this.logger.log(`Finished queuing ${count} asset check(s) for library ${library.id}`); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async findOrFail(id: string) { diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 3b9eafde8f..0f4ba769c0 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,5 +1,6 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { Exif } from 'src/database'; import { AssetFileType, @@ -12,7 +13,7 @@ import { JobName, JobStatus, RawExtractedFormat, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, } from 'src/enum'; @@ -49,7 +50,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.image.id }, }, ]); @@ -57,7 +58,7 @@ describe(MediaService.name, () => { expect(mocks.person.getAll).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.newThumbnail.id }, }, ]); @@ -72,7 +73,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.trashed.id }, }, ]); @@ -87,7 +88,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.archived.id }, }, ]); @@ -106,7 +107,7 @@ describe(MediaService.name, () => { expect(mocks.person.update).toHaveBeenCalledTimes(1); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.newThumbnail.id, }, @@ -122,7 +123,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.image.id }, }, ]); @@ -138,7 +139,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.image.id }, }, ]); @@ -154,7 +155,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: assetStub.image.id }, }, ]); @@ -169,14 +170,14 @@ describe(MediaService.name, () => { mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts); mocks.person.getAll.mockReturnValue(makeStream([personStub.withName])); - await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success); expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.MIGRATE_ASSET, data: { id: assetStub.image.id } }, + { name: JobName.AssetFileMigration, data: { id: assetStub.image.id } }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.MIGRATE_PERSON, data: { id: personStub.withName.id } }, + { name: JobName.PersonFileMigration, data: { id: personStub.withName.id } }, ]); }); }); @@ -184,7 +185,7 @@ describe(MediaService.name, () => { describe('handleAssetMigration', () => { it('should fail if asset does not exist', async () => { mocks.assetJob.getForMigrationJob.mockResolvedValue(void 0); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); expect(mocks.move.getByEntity).not.toHaveBeenCalled(); }); @@ -196,27 +197,27 @@ describe(MediaService.name, () => { id: 'move-id', newPath: '/new/path', oldPath: '/old/path', - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, }); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.FULLSIZE, + pathType: AssetPathType.FullSize, oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.PREVIEW, + pathType: AssetPathType.Preview, oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-preview.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.THUMBNAIL, + pathType: AssetPathType.Thumbnail, oldPath: '/uploads/user-id/webp/path.ext', - newPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + newPath: expect.stringContaining('upload/thumbs/user-id/as/se/asset-id-thumbnail.webp'), }); expect(mocks.move.create).toHaveBeenCalledTimes(3); }); @@ -253,7 +254,7 @@ describe(MediaService.name, () => { it('should skip thumbnail generation if asset type is unknown', async () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ ...assetStub.image, type: 'foo' as AssetType }); - await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.Skipped); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); @@ -270,14 +271,14 @@ describe(MediaService.name, () => { it('should skip invisible assets', async () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.livePhotoMotionAsset); - expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should delete previous preview if different path', async () => { - mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } }); + mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -295,7 +296,7 @@ describe(MediaService.name, () => { await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { @@ -309,25 +310,25 @@ describe(MediaService.name, () => { rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, size: 1440, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.WEBP, + format: ImageFormat.Webp, size: 250, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + expect.any(String), ); expect(mocks.media.generateThumbhash).toHaveBeenCalledOnce(); @@ -340,13 +341,13 @@ describe(MediaService.name, () => { expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + type: AssetFileType.Preview, + path: expect.any(String), }, { assetId: 'asset-id', - type: AssetFileType.THUMBNAIL, - path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + type: AssetFileType.Thumbnail, + path: expect.any(String), }, ]); expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); @@ -357,10 +358,10 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); await sut.handleGenerateThumbnails({ id: assetStub.video.id }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ @@ -376,13 +377,13 @@ describe(MediaService.name, () => { expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + type: AssetFileType.Preview, + path: expect.any(String), }, { assetId: 'asset-id', - type: AssetFileType.THUMBNAIL, - path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + type: AssetFileType.Thumbnail, + path: expect.any(String), }, ]); }); @@ -392,10 +393,10 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); await sut.handleGenerateThumbnails({ id: assetStub.video.id }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ @@ -411,13 +412,13 @@ describe(MediaService.name, () => { expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + type: AssetFileType.Preview, + path: expect.any(String), }, { assetId: 'asset-id', - type: AssetFileType.THUMBNAIL, - path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + type: AssetFileType.Thumbnail, + path: expect.any(String), }, ]); }); @@ -432,7 +433,7 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ @@ -453,7 +454,7 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), expect.objectContaining({ inputOptions: ['-sws_flags accurate_rnd+full_chroma_int'], outputOptions: expect.any(Array), @@ -465,13 +466,13 @@ describe(MediaService.name, () => { it('should use scaling divisible by 2 even when using quick sync', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); await sut.handleGenerateThumbnails({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringContaining('scale=-2:1440')]), @@ -485,15 +486,15 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`; - const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.webp`; + const previewPath = APP_MEDIA_LOCATION + `/thumbs/user-id/as/se/asset-id-preview.${format}`; + const thumbnailPath = APP_MEDIA_LOCATION + `/thumbs/user-id/as/se/asset-id-thumbnail.webp`; await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { - colorspace: Colorspace.SRGB, + colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, }); @@ -502,7 +503,7 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { - colorspace: Colorspace.SRGB, + colorspace: Colorspace.Srgb, format, size: 1440, quality: 80, @@ -514,8 +515,8 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { - colorspace: Colorspace.SRGB, - format: ImageFormat.WEBP, + colorspace: Colorspace.Srgb, + format: ImageFormat.Webp, size: 250, quality: 80, processInvalidImages: false, @@ -530,15 +531,15 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.jpeg`; - const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`; + const previewPath = expect.stringContaining(`upload/thumbs/user-id/as/se/asset-id-preview.jpeg`); + const thumbnailPath = expect.stringContaining(`upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { - colorspace: Colorspace.SRGB, + colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, }); @@ -547,8 +548,8 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { - colorspace: Colorspace.SRGB, - format: ImageFormat.JPEG, + colorspace: Colorspace.Srgb, + format: ImageFormat.Jpeg, size: 1440, quality: 80, processInvalidImages: false, @@ -559,7 +560,7 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { - colorspace: Colorspace.SRGB, + colorspace: Colorspace.Srgb, format, size: 250, quality: 80, @@ -571,7 +572,7 @@ describe(MediaService.name, () => { }); it('should delete previous thumbnail if different path', async () => { - mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } }); + mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -580,7 +581,7 @@ describe(MediaService.name, () => { }); it('should extract embedded image if enabled and available', async () => { - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); @@ -596,7 +597,7 @@ describe(MediaService.name, () => { }); it('should resize original image if embedded image is too small', async () => { - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); @@ -658,12 +659,12 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.objectContaining({ processInvalidImages: false }), - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.objectContaining({ processInvalidImages: false }), - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + expect.any(String), ); expect(mocks.media.generateThumbhash).toHaveBeenCalledOnce(); @@ -678,9 +679,9 @@ describe(MediaService.name, () => { it('should extract full-size JPEG preview from RAW', async () => { mocks.systemMetadata.get.mockResolvedValue({ - image: { fullsize: { enabled: true, format: ImageFormat.WEBP }, extractEmbedded: true }, + image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); @@ -698,21 +699,21 @@ describe(MediaService.name, () => { fullsizeBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, size: 1440, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), ); }); it('should convert full-size WEBP preview from JXL preview of RAW', async () => { mocks.systemMetadata.get.mockResolvedValue({ - image: { fullsize: { enabled: true, format: ImageFormat.WEBP }, extractEmbedded: true }, + image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JXL }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); @@ -729,30 +730,30 @@ describe(MediaService.name, () => { fullsizeBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.WEBP, + format: ImageFormat.Webp, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-fullsize.webp', + expect.any(String), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( fullsizeBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, size: 1440, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), ); }); it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); @@ -769,30 +770,30 @@ describe(MediaService.name, () => { rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + expect.any(String), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, size: 1440, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.any(String), ); }); it('should generate full-size preview from non-web-friendly images', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif); @@ -810,18 +811,18 @@ describe(MediaService.name, () => { rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + expect.any(String), ); }); it('should skip generating full-size preview for web-friendly images', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); @@ -829,7 +830,7 @@ describe(MediaService.name, () => { expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { - colorspace: Colorspace.SRGB, + colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, }); @@ -838,15 +839,15 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).not.toHaveBeenCalledWith( expect.anything(), expect.anything(), - 'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg', + expect.stringContaining('fullsize.jpeg'), ); }); it('should respect encoding options when generating full-size preview', async () => { mocks.systemMetadata.get.mockResolvedValue({ - image: { fullsize: { enabled: true, format: ImageFormat.WEBP, quality: 90 } }, + image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } }, }); - mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif); @@ -864,12 +865,12 @@ describe(MediaService.name, () => { rawBuffer, { colorspace: Colorspace.P3, - format: ImageFormat.WEBP, + format: ImageFormat.Webp, quality: 90, processInvalidImages: false, raw: rawInfo, }, - 'upload/thumbs/user-id/as/se/asset-id-fullsize.webp', + expect.any(String), ); }); }); @@ -878,7 +879,7 @@ describe(MediaService.name, () => { it('should skip if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.asset.getByIds).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); @@ -907,11 +908,11 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.originalPath, { colorspace: Colorspace.P3, orientation: undefined, @@ -921,7 +922,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 238, @@ -933,12 +934,9 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ - id: 'person-1', - thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', - }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); }); it('should use preview path if video', async () => { @@ -949,11 +947,11 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.previewPath, { colorspace: Colorspace.P3, orientation: undefined, @@ -963,7 +961,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 238, @@ -975,12 +973,9 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ - id: 'person-1', - thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', - }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); }); it('should generate a thumbnail without going negative', async () => { @@ -991,7 +986,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailStart.originalPath, { @@ -1003,7 +998,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 0, @@ -1015,7 +1010,7 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); }); @@ -1028,7 +1023,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailEnd.originalPath, { @@ -1040,7 +1035,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 591, @@ -1052,7 +1047,7 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); }); @@ -1065,7 +1060,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.negativeCoordinate.originalPath, { @@ -1077,7 +1072,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 0, @@ -1089,7 +1084,7 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); }); @@ -1102,7 +1097,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.overflowingCoordinate.originalPath, { @@ -1114,7 +1109,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { left: 4485, @@ -1126,7 +1121,7 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); }); @@ -1138,12 +1133,12 @@ describe(MediaService.name, () => { const extracted = Buffer.from(''); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; - mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.decodeImage.mockResolvedValue({ data, info }); mocks.media.getImageDimensions.mockResolvedValue(info); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); @@ -1156,7 +1151,7 @@ describe(MediaService.name, () => { data, { colorspace: Colorspace.P3, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, crop: { height: 844, @@ -1168,7 +1163,7 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 250, }, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + expect.any(String), ); }); @@ -1180,7 +1175,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.extract).not.toHaveBeenCalled(); @@ -1196,7 +1191,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); @@ -1216,11 +1211,11 @@ describe(MediaService.name, () => { const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.JPEG }); + mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue(info); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.SUCCESS, + JobStatus.Success, ); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); @@ -1243,7 +1238,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForVideoConversion).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.VIDEO_CONVERSION, + name: JobName.AssetEncodeVideo, data: { id: assetStub.video.id }, }, ]); @@ -1257,7 +1252,7 @@ describe(MediaService.name, () => { expect(mocks.assetJob.streamForVideoConversion).toHaveBeenCalledWith(void 0); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.VIDEO_CONVERSION, + name: JobName.AssetEncodeVideo, data: { id: assetStub.video.id }, }, ]); @@ -1288,7 +1283,7 @@ describe(MediaService.name, () => { expect(mocks.storage.mkdirSync).toHaveBeenCalled(); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-map 0:1', '-map 0:3']), @@ -1308,7 +1303,7 @@ describe(MediaService.name, () => { expect(mocks.storage.mkdirSync).toHaveBeenCalled(); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:2']), @@ -1340,21 +1335,21 @@ describe(MediaService.name, () => { it('should throw an error if transcoding fails and hw acceleration is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { transcode: TranscodePolicy.ALL, accel: TranscodeHWAccel.DISABLED }, + ffmpeg: { transcode: TranscodePolicy.All, accel: TranscodeHardwareAcceleration.Disabled }, }); mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video')); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.Failed); expect(mocks.media.transcode).toHaveBeenCalledTimes(1); }); it('should transcode when set to all', async () => { mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), @@ -1365,11 +1360,11 @@ describe(MediaService.name, () => { it('should transcode when optimal and too big', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), @@ -1380,11 +1375,11 @@ describe(MediaService.name, () => { it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: '30M' } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '30M' } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), @@ -1395,11 +1390,11 @@ describe(MediaService.name, () => { it('should transcode when max bitrate is not a number', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: 'foo' } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: 'foo' } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), @@ -1411,12 +1406,12 @@ describe(MediaService.name, () => { it('should not scale resolution if no target resolution', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' }, + ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('scale')]), @@ -1427,11 +1422,11 @@ describe(MediaService.name, () => { it('should scale horizontally when video is horizontal', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:720/)]), @@ -1442,11 +1437,11 @@ describe(MediaService.name, () => { it('should scale vertically when video is vertical', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=720:-2/)]), @@ -1458,12 +1453,12 @@ describe(MediaService.name, () => { it('should always scale video if height is uneven', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamOddHeight); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' }, + ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:354/)]), @@ -1475,12 +1470,12 @@ describe(MediaService.name, () => { it('should always scale video if width is uneven', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamOddWidth); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' }, + ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=354:-2/)]), @@ -1492,12 +1487,12 @@ describe(MediaService.name, () => { it('should copy video stream when video matches target', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { targetVideoCodec: VideoCodec.HEVC, acceptedAudioCodecs: [AudioCodec.AAC] }, + ffmpeg: { targetVideoCodec: VideoCodec.Hevc, acceptedAudioCodecs: [AudioCodec.Aac] }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-c:a aac']), @@ -1510,15 +1505,15 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamH264); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - targetVideoCodec: VideoCodec.HEVC, - acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC], - acceptedAudioCodecs: [AudioCodec.AAC], + targetVideoCodec: VideoCodec.Hevc, + acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.Hevc], + acceptedAudioCodecs: [AudioCodec.Aac], }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining(['-tag:v hvc1']), @@ -1531,15 +1526,15 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - targetVideoCodec: VideoCodec.HEVC, - acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC], - acceptedAudioCodecs: [AudioCodec.AAC], + targetVideoCodec: VideoCodec.Hevc, + acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.Hevc], + acceptedAudioCodecs: [AudioCodec.Aac], }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-tag:v hvc1']), @@ -1550,11 +1545,11 @@ describe(MediaService.name, () => { it('should copy audio stream when audio matches target', async () => { mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']), @@ -1568,7 +1563,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy']), @@ -1587,14 +1582,14 @@ describe(MediaService.name, () => { it('should not transcode if transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not remux when input is not an accepted container and transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1609,14 +1604,14 @@ describe(MediaService.name, () => { it('should delete existing transcode if current policy does not require transcoding', async () => { const asset = assetStub.hasEncodedVideo; mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); await sut.handleVideoConversion({ id: asset.id }); expect(mocks.media.transcode).not.toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.DELETE_FILES, + name: JobName.FileDelete, data: { files: [asset.encodedVideoPath] }, }); }); @@ -1627,7 +1622,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']), @@ -1642,7 +1637,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']), @@ -1657,7 +1652,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']), @@ -1672,7 +1667,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']), @@ -1687,13 +1682,13 @@ describe(MediaService.name, () => { ffmpeg: { maxBitrate: '4500k', twoPass: true, - targetVideoCodec: VideoCodec.VP9, + targetVideoCodec: VideoCodec.Vp9, }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']), @@ -1708,13 +1703,13 @@ describe(MediaService.name, () => { ffmpeg: { maxBitrate: '0', twoPass: true, - targetVideoCodec: VideoCodec.VP9, + targetVideoCodec: VideoCodec.Vp9, }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-maxrate')]), @@ -1725,11 +1720,11 @@ describe(MediaService.name, () => { it('should configure preset for vp9', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, preset: 'slow' } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, preset: 'slow' } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-cpu-used 2']), @@ -1740,11 +1735,11 @@ describe(MediaService.name, () => { it('should not configure preset for vp9 if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.VP9 } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.Vp9 } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-cpu-used')]), @@ -1755,11 +1750,11 @@ describe(MediaService.name, () => { it('should configure threads if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, threads: 2 } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, threads: 2 } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-threads 2']), @@ -1774,7 +1769,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-threads 1', '-x264-params frame-threads=1:pools=none']), @@ -1789,7 +1784,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]), @@ -1800,11 +1795,11 @@ describe(MediaService.name, () => { it('should disable thread pooling for hevc if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.HEVC } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.Hevc } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v hevc', '-threads 1', '-x265-params frame-threads=1:pools=none']), @@ -1815,11 +1810,11 @@ describe(MediaService.name, () => { it('should omit thread flags for hevc if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.HEVC } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.Hevc } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]), @@ -1830,11 +1825,11 @@ describe(MediaService.name, () => { it('should use av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1 } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1 } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([ @@ -1855,11 +1850,11 @@ describe(MediaService.name, () => { it('should map `veryslow` preset to 4 for av1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, preset: 'veryslow' } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, preset: 'veryslow' } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-preset 4']), @@ -1870,11 +1865,11 @@ describe(MediaService.name, () => { it('should set max bitrate for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, maxBitrate: '2M' } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, maxBitrate: '2M' } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params mbr=2M']), @@ -1885,11 +1880,11 @@ describe(MediaService.name, () => { it('should set threads for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4 } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4 } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params lp=4']), @@ -1901,12 +1896,12 @@ describe(MediaService.name, () => { it('should set both bitrate and threads for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4, maxBitrate: '2M' }, + ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4, maxBitrate: '2M' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params lp=4:mbr=2M']), @@ -1919,8 +1914,8 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - targetVideoCodec: VideoCodec.HEVC, - transcode: TranscodePolicy.OPTIMAL, + targetVideoCodec: VideoCodec.Hevc, + transcode: TranscodePolicy.Optimal, targetResolution: '1080p', }, }); @@ -1931,7 +1926,7 @@ describe(MediaService.name, () => { it('should fail if hwaccel is enabled for an unsupported codec', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, targetVideoCodec: VideoCodec.Vp9 }, }); await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); @@ -1946,11 +1941,11 @@ describe(MediaService.name, () => { it('should set options for nvenc', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining([ @@ -1979,7 +1974,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - accel: TranscodeHWAccel.NVENC, + accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k', twoPass: true, }, @@ -1987,7 +1982,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining([expect.stringContaining('-multipass')]), @@ -1998,11 +1993,13 @@ describe(MediaService.name, () => { it('should set vbr options for nvenc when max bitrate is enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining(['-cq:v 23', '-maxrate 10000k', '-bufsize 6897k']), @@ -2013,11 +2010,13 @@ describe(MediaService.name, () => { it('should set cq options for nvenc when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.stringContaining('-maxrate'), @@ -2028,11 +2027,13 @@ describe(MediaService.name, () => { it('should omit preset for nvenc if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, preset: 'invalid' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, preset: 'invalid' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]), @@ -2043,11 +2044,11 @@ describe(MediaService.name, () => { it('should ignore two pass for nvenc if max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.arrayContaining([expect.stringContaining('-multipass')]), @@ -2059,12 +2060,12 @@ describe(MediaService.name, () => { it('should use hardware decoding for nvenc if enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel cuda', @@ -2081,12 +2082,12 @@ describe(MediaService.name, () => { it('should use hardware tone-mapping for nvenc if hardware decoding is enabled and should tone map', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']), outputOptions: expect.arrayContaining([ @@ -2102,12 +2103,12 @@ describe(MediaService.name, () => { it('should set format to nv12 for nvenc if input is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']), outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]), @@ -2118,11 +2119,13 @@ describe(MediaService.name, () => { it('should set options for qsv', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', @@ -2154,7 +2157,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - accel: TranscodeHWAccel.QSV, + accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k', preferredHwDevice: '/dev/dri/renderD128', }, @@ -2162,7 +2165,7 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', @@ -2176,11 +2179,13 @@ describe(MediaService.name, () => { it('should omit preset for qsv if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, preset: 'invalid' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', @@ -2195,12 +2200,12 @@ describe(MediaService.name, () => { it('should set low power mode for qsv if target video codec is vp9', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, targetVideoCodec: VideoCodec.Vp9 }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', @@ -2215,7 +2220,7 @@ describe(MediaService.name, () => { it('should fail for qsv if no hw devices', async () => { sut.videoInterfaces = { dri: [], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); @@ -2225,11 +2230,11 @@ describe(MediaService.name, () => { it('should prefer higher index renderD* device for qsv', async () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD129', @@ -2244,14 +2249,14 @@ describe(MediaService.name, () => { it('should use hardware decoding for qsv if enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel qsv', @@ -2270,14 +2275,14 @@ describe(MediaService.name, () => { it('should use hardware tone-mapping for qsv if hardware decoding is enabled and should tone map', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel qsv', @@ -2299,13 +2304,13 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true, preferredHwDevice: 'renderD129' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true, preferredHwDevice: 'renderD129' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel qsv', '-qsv_device /dev/dri/renderD129']), outputOptions: expect.any(Array), @@ -2317,14 +2322,14 @@ describe(MediaService.name, () => { it('should set format to nv12 for qsv if input is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel qsv', @@ -2340,11 +2345,11 @@ describe(MediaService.name, () => { it('should set options for vaapi', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2370,11 +2375,13 @@ describe(MediaService.name, () => { it('should set vbr options for vaapi when max bitrate is enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, maxBitrate: '10000k' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2394,11 +2401,11 @@ describe(MediaService.name, () => { it('should set cq options for vaapi when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2418,11 +2425,13 @@ describe(MediaService.name, () => { it('should omit preset for vaapi if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preset: 'invalid' }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2437,11 +2446,11 @@ describe(MediaService.name, () => { it('should prefer higher index renderD* device for vaapi', async () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD129', @@ -2457,12 +2466,12 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['renderD129', 'card1', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preferredHwDevice: '/dev/dri/renderD128' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2477,14 +2486,14 @@ describe(MediaService.name, () => { it('should use hardware decoding for vaapi if enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel vaapi', @@ -2502,14 +2511,14 @@ describe(MediaService.name, () => { it('should use hardware tone-mapping for vaapi if hardware decoding is enabled and should tone map', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']), outputOptions: expect.arrayContaining([ @@ -2525,14 +2534,14 @@ describe(MediaService.name, () => { it('should set format to nv12 for vaapi if input is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']), outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]), @@ -2545,13 +2554,13 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true, preferredHwDevice: 'renderD129' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true, preferredHwDevice: 'renderD129' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_device /dev/dri/renderD129']), outputOptions: expect.any(Array), @@ -2562,13 +2571,15 @@ describe(MediaService.name, () => { it('should fallback to hw encoding and sw decoding if hw transcoding fails and hw decoding is enabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, + }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', @@ -2582,14 +2593,16 @@ describe(MediaService.name, () => { it('should fallback to sw decoding if fallback to sw decoding + hw encoding fails', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, + }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledTimes(3); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264']), @@ -2600,13 +2613,13 @@ describe(MediaService.name, () => { it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264']), @@ -2618,18 +2631,20 @@ describe(MediaService.name, () => { it('should fail for vaapi if no hw devices', async () => { sut.videoInterfaces = { dri: [], mali: true }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should set options for rkmpp', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } }); + mocks.systemMetadata.get.mockResolvedValue({ + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true }, + }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel rkmpp', @@ -2660,16 +2675,16 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { - accel: TranscodeHWAccel.RKMPP, + accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, maxBitrate: '10000k', - targetVideoCodec: VideoCodec.HEVC, + targetVideoCodec: VideoCodec.Hevc, }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([`-c:v hevc_rkmpp`, '-level 153', '-rc_mode AVBR', '-b:v 10000k']), @@ -2681,12 +2696,12 @@ describe(MediaService.name, () => { it('should set cqp options for rkmpp when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([`-c:v h264_rkmpp`, '-level 51', '-rc_mode CQP', '-qp_init 30']), @@ -2698,12 +2713,12 @@ describe(MediaService.name, () => { it('should set OpenCL tonemapping options for rkmpp when OpenCL is available', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([ @@ -2720,12 +2735,12 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([ @@ -2739,12 +2754,12 @@ describe(MediaService.name, () => { it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: false, crf: 30, maxBitrate: '0' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: [], outputOptions: expect.arrayContaining([ @@ -2761,12 +2776,12 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ - ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, + ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([ @@ -2781,11 +2796,11 @@ describe(MediaService.name, () => { it('should tonemap when policy is required and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([ @@ -2800,11 +2815,11 @@ describe(MediaService.name, () => { it('should tonemap when policy is optimal and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([ @@ -2819,11 +2834,11 @@ describe(MediaService.name, () => { it('should transcode when policy is required and video is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy', '-vf format=yuv420p']), @@ -2834,11 +2849,11 @@ describe(MediaService.name, () => { it('should convert to yuv420p when scaling without tone-mapping', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit); - mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); + mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.any(String), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy', '-vf scale=-2:720,format=yuv420p']), @@ -2854,19 +2869,15 @@ describe(MediaService.name, () => { await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true }); - expect(mocks.media.transcode).toHaveBeenCalledWith( - assetStub.video.originalPath, - 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { - inputOptions: expect.any(Array), - outputOptions: expect.any(Array), - twoPass: false, - progress: { - frameCount: probeStub.videoStream2160p.videoStreams[0].frameCount, - percentInterval: expect.any(Number), - }, + expect(mocks.media.transcode).toHaveBeenCalledWith(assetStub.video.originalPath, expect.any(String), { + inputOptions: expect.any(Array), + outputOptions: expect.any(Array), + twoPass: false, + progress: { + frameCount: probeStub.videoStream2160p.videoStreams[0].frameCount, + percentInterval: expect.any(Number), }, - ); + }); }); it('should not count frames for progress when log level is not debug', async () => { @@ -2884,7 +2895,7 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', + APP_MEDIA_LOCATION + '/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:a copy']), diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 452e4df5eb..a2c3c5ed42 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -18,7 +18,7 @@ import { QueueName, RawExtractedFormat, StorageFolder, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, TranscodeTarget, VideoCodec, @@ -57,8 +57,8 @@ export class MediaService extends BaseService { this.videoInterfaces = { dri, mali }; } - @OnJob({ name: JobName.QUEUE_GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION }) - async handleQueueGenerateThumbnails({ force }: JobOf): Promise { + @OnJob({ name: JobName.AssetGenerateThumbnailsQueueAll, queue: QueueName.ThumbnailGeneration }) + async handleQueueGenerateThumbnails({ force }: JobOf): Promise { let jobs: JobItem[] = []; const queueAll = async () => { @@ -70,7 +70,7 @@ export class MediaService extends BaseService { const { previewFile, thumbnailFile } = getAssetFiles(asset.files); if (!previewFile || !thumbnailFile || !asset.thumbhash || force) { - jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id: asset.id } }); + jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { @@ -92,7 +92,7 @@ export class MediaService extends BaseService { await this.personRepository.update({ id: person.id, faceAssetId: face.id }); } - jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } }); + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -100,21 +100,21 @@ export class MediaService extends BaseService { await queueAll(); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.QUEUE_MIGRATION, queue: QueueName.MIGRATION }) + @OnJob({ name: JobName.FileMigrationQueueAll, queue: QueueName.Migration }) async handleQueueMigration(): Promise { - const { active, waiting } = await this.jobRepository.getJobCounts(QueueName.MIGRATION); + const { active, waiting } = await this.jobRepository.getJobCounts(QueueName.Migration); if (active === 1 && waiting === 0) { - await this.storageCore.removeEmptyDirs(StorageFolder.THUMBNAILS); - await this.storageCore.removeEmptyDirs(StorageFolder.ENCODED_VIDEO); + await this.storageCore.removeEmptyDirs(StorageFolder.Thumbnails); + await this.storageCore.removeEmptyDirs(StorageFolder.EncodedVideo); } let jobs: JobItem[] = []; const assets = this.assetJobRepository.streamForMigrationJob(); for await (const asset of assets) { - jobs.push({ name: JobName.MIGRATE_ASSET, data: { id: asset.id } }); + jobs.push({ name: JobName.AssetFileMigration, data: { id: asset.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(jobs); jobs = []; @@ -125,7 +125,7 @@ export class MediaService extends BaseService { jobs = []; for await (const person of this.personRepository.getAll()) { - jobs.push({ name: JobName.MIGRATE_PERSON, data: { id: person.id } }); + jobs.push({ name: JobName.PersonFileMigration, data: { id: person.id } }); if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(jobs); @@ -135,36 +135,36 @@ export class MediaService extends BaseService { await this.jobRepository.queueAll(jobs); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.MIGRATE_ASSET, queue: QueueName.MIGRATION }) - async handleAssetMigration({ id }: JobOf): Promise { + @OnJob({ name: JobName.AssetFileMigration, queue: QueueName.Migration }) + async handleAssetMigration({ id }: JobOf): Promise { const { image } = await this.getConfig({ withCache: true }); const asset = await this.assetJobRepository.getForMigrationJob(id); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } - await this.storageCore.moveAssetImage(asset, AssetPathType.FULLSIZE, image.fullsize.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); + await this.storageCore.moveAssetImage(asset, AssetPathType.FullSize, image.fullsize.format); + await this.storageCore.moveAssetImage(asset, AssetPathType.Preview, image.preview.format); + await this.storageCore.moveAssetImage(asset, AssetPathType.Thumbnail, image.thumbnail.format); await this.storageCore.moveAssetVideo(asset); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION }) - async handleGenerateThumbnails({ id }: JobOf): Promise { + @OnJob({ name: JobName.AssetGenerateThumbnails, queue: QueueName.ThumbnailGeneration }) + async handleGenerateThumbnails({ id }: JobOf): Promise { const asset = await this.assetJobRepository.getForGenerateThumbnailJob(id); if (!asset) { this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`); - return JobStatus.FAILED; + return JobStatus.Failed; } - if (asset.visibility === AssetVisibility.HIDDEN) { + if (asset.visibility === AssetVisibility.Hidden) { this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } let generated: { @@ -173,27 +173,27 @@ export class MediaService extends BaseService { fullsizePath?: string; thumbhash: Buffer; }; - if (asset.type === AssetType.VIDEO || asset.originalFileName.toLowerCase().endsWith('.gif')) { + if (asset.type === AssetType.Video || asset.originalFileName.toLowerCase().endsWith('.gif')) { generated = await this.generateVideoThumbnails(asset); - } else if (asset.type === AssetType.IMAGE) { + } else if (asset.type === AssetType.Image) { generated = await this.generateImageThumbnails(asset); } else { this.logger.warn(`Skipping thumbnail generation for asset ${id}: ${asset.type} is not an image or video`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { previewFile, thumbnailFile, fullsizeFile } = getAssetFiles(asset.files); const toUpsert: UpsertFileOptions[] = []; if (previewFile?.path !== generated.previewPath) { - toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.PREVIEW }); + toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.Preview }); } if (thumbnailFile?.path !== generated.thumbnailPath) { - toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.THUMBNAIL }); + toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.Thumbnail }); } if (generated.fullsizePath && fullsizeFile?.path !== generated.fullsizePath) { - toUpsert.push({ assetId: asset.id, path: generated.fullsizePath, type: AssetFileType.FULLSIZE }); + toUpsert.push({ assetId: asset.id, path: generated.fullsizePath, type: AssetFileType.FullSize }); } if (toUpsert.length > 0) { @@ -230,7 +230,7 @@ export class MediaService extends BaseService { await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date(), thumbnailAt: new Date() }); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async extractImage(originalPath: string, minSize: number) { @@ -244,7 +244,7 @@ export class MediaService extends BaseService { private async decodeImage(thumbSource: string | Buffer, exifInfo: Exif, targetSize?: number) { const { image } = await this.getConfig({ withCache: true }); - const colorspace = this.isSRGB(exifInfo) ? Colorspace.SRGB : image.colorspace; + const colorspace = this.isSRGB(exifInfo) ? Colorspace.Srgb : image.colorspace; const decodeOptions: DecodeToBufferOptions = { colorspace, processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', @@ -264,8 +264,8 @@ export class MediaService extends BaseService { exifInfo: Exif; }) { const { image } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format); - const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); + const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format); + const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); // Handle embedded preview extraction for RAW files @@ -294,11 +294,11 @@ export class MediaService extends BaseService { if (convertFullsize) { // convert a new fullsize image from the same source as the thumbnail - fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, image.fullsize.format); + fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, image.fullsize.format); const fullsizeOptions = { format: image.fullsize.format, quality: image.fullsize.quality, ...thumbnailOptions }; promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath)); - } else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.JPEG) { - fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, extracted.format); + } else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) { + fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format); this.storageCore.ensureFolders(fullsizePath); // Write the buffer to disk with essential EXIF data @@ -317,25 +317,25 @@ export class MediaService extends BaseService { return { previewPath, thumbnailPath, fullsizePath, thumbhash: outputs[0] as Buffer }; } - @OnJob({ name: JobName.GENERATE_PERSON_THUMBNAIL, queue: QueueName.THUMBNAIL_GENERATION }) - async handleGeneratePersonThumbnail({ id }: JobOf): Promise { + @OnJob({ name: JobName.PersonGenerateThumbnail, queue: QueueName.ThumbnailGeneration }) + async handleGeneratePersonThumbnail({ id }: JobOf): Promise { const { machineLearning, metadata, image } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const data = await this.personRepository.getDataForThumbnailGenerationJob(id); if (!data) { this.logger.error(`Could not generate person thumbnail for ${id}: missing data`); - return JobStatus.FAILED; + return JobStatus.Failed; } const { ownerId, x1, y1, x2, y2, oldWidth, oldHeight, exifOrientation, previewPath, originalPath } = data; let inputImage: string | Buffer; - if (data.type === AssetType.VIDEO) { + if (data.type === AssetType.Video) { if (!previewPath) { this.logger.error(`Could not generate person thumbnail for video ${id}: missing preview path`); - return JobStatus.FAILED; + return JobStatus.Failed; } inputImage = previewPath; } else if (image.extractEmbedded && mimeTypes.isRaw(originalPath)) { @@ -357,7 +357,7 @@ export class MediaService extends BaseService { const thumbnailOptions = { colorspace: image.colorspace, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, raw: info, quality: image.thumbnail.quality, crop: this.getCrop( @@ -371,7 +371,7 @@ export class MediaService extends BaseService { await this.mediaRepository.generateThumbnail(decodedImage, thumbnailOptions, thumbnailPath); await this.personRepository.update({ id, thumbnailPath }); - return JobStatus.SUCCESS; + return JobStatus.Success; } private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { @@ -411,8 +411,8 @@ export class MediaService extends BaseService { private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) { const { image, ffmpeg } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format); - const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); + const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format); + const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); @@ -424,9 +424,9 @@ export class MediaService extends BaseService { const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() }); const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() }); - const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream, format); + const previewOptions = previewConfig.getCommand(TranscodeTarget.Video, mainVideoStream, mainAudioStream, format); const thumbnailOptions = thumbnailConfig.getCommand( - TranscodeTarget.VIDEO, + TranscodeTarget.Video, mainVideoStream, mainAudioStream, format, @@ -443,13 +443,13 @@ export class MediaService extends BaseService { return { previewPath, thumbnailPath, thumbhash }; } - @OnJob({ name: JobName.QUEUE_VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION }) - async handleQueueVideoConversion(job: JobOf): Promise { + @OnJob({ name: JobName.AssetEncodeVideoQueueAll, queue: QueueName.VideoConversion }) + async handleQueueVideoConversion(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.VIDEO_CONVERSION; data: { id: string } }[] = []; + let queue: { name: JobName.AssetEncodeVideo; data: { id: string } }[] = []; for await (const asset of this.assetJobRepository.streamForVideoConversion(force)) { - queue.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } }); + queue.push({ name: JobName.AssetEncodeVideo, data: { id: asset.id } }); if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(queue); @@ -459,14 +459,14 @@ export class MediaService extends BaseService { await this.jobRepository.queueAll(queue); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION }) - async handleVideoConversion({ id }: JobOf): Promise { + @OnJob({ name: JobName.AssetEncodeVideo, queue: QueueName.VideoConversion }) + async handleVideoConversion({ id }: JobOf): Promise { const asset = await this.assetJobRepository.getForVideoConversion(id); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } const input = asset.originalPath; @@ -474,35 +474,35 @@ export class MediaService extends BaseService { this.storageCore.ensureFolders(output); const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, { - countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs + countFrames: this.logger.isLevelEnabled(LogLevel.Debug), // makes frame count more reliable for progress logs }); const videoStream = this.getMainStream(videoStreams); const audioStream = this.getMainStream(audioStreams); if (!videoStream || !format.formatName) { - return JobStatus.FAILED; + return JobStatus.Failed; } if (!videoStream.height || !videoStream.width) { this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`); - return JobStatus.FAILED; + return JobStatus.Failed; } let { ffmpeg } = await this.getConfig({ withCache: true }); const target = this.getTranscodeTarget(ffmpeg, videoStream, audioStream); - if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) { + if (target === TranscodeTarget.None && !this.isRemuxRequired(ffmpeg, format)) { if (asset.encodedVideoPath) { this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`); - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [asset.encodedVideoPath] } }); await this.assetRepository.update({ id: asset.id, encodedVideoPath: null }); } else { this.logger.verbose(`Asset ${asset.id} does not require transcoding based on current policy, skipping`); } - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); - if (ffmpeg.accel === TranscodeHWAccel.DISABLED) { + if (ffmpeg.accel === TranscodeHardwareAcceleration.Disabled) { this.logger.log(`Transcoding video ${asset.id} without hardware acceleration`); } else { this.logger.log( @@ -514,8 +514,8 @@ export class MediaService extends BaseService { await this.mediaRepository.transcode(input, output, command); } catch (error: any) { this.logger.error(`Error occurred during transcoding: ${error.message}`); - if (ffmpeg.accel === TranscodeHWAccel.DISABLED) { - return JobStatus.FAILED; + if (ffmpeg.accel === TranscodeHardwareAcceleration.Disabled) { + return JobStatus.Failed; } let partialFallbackSuccess = false; @@ -533,7 +533,7 @@ export class MediaService extends BaseService { if (!partialFallbackSuccess) { this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`); - ffmpeg = { ...ffmpeg, accel: TranscodeHWAccel.DISABLED }; + ffmpeg = { ...ffmpeg, accel: TranscodeHardwareAcceleration.Disabled }; const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); await this.mediaRepository.transcode(input, output, command); } @@ -543,7 +543,7 @@ export class MediaService extends BaseService { await this.assetRepository.update({ id: asset.id, encodedVideoPath: output }); - return JobStatus.SUCCESS; + return JobStatus.Success; } private getMainStream(streams: T[]): T { @@ -561,18 +561,18 @@ export class MediaService extends BaseService { const isVideoTranscodeRequired = this.isVideoTranscodeRequired(config, videoStream); if (isAudioTranscodeRequired && isVideoTranscodeRequired) { - return TranscodeTarget.ALL; + return TranscodeTarget.All; } if (isAudioTranscodeRequired) { - return TranscodeTarget.AUDIO; + return TranscodeTarget.Audio; } if (isVideoTranscodeRequired) { - return TranscodeTarget.VIDEO; + return TranscodeTarget.Video; } - return TranscodeTarget.NONE; + return TranscodeTarget.None; } private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: AudioStreamInfo): boolean { @@ -581,15 +581,15 @@ export class MediaService extends BaseService { } switch (ffmpegConfig.transcode) { - case TranscodePolicy.DISABLED: { + case TranscodePolicy.Disabled: { return false; } - case TranscodePolicy.ALL: { + case TranscodePolicy.All: { return true; } - case TranscodePolicy.REQUIRED: - case TranscodePolicy.OPTIMAL: - case TranscodePolicy.BITRATE: { + case TranscodePolicy.Required: + case TranscodePolicy.Optimal: + case TranscodePolicy.Bitrate: { return !ffmpegConfig.acceptedAudioCodecs.includes(stream.codecName as AudioCodec); } default: { @@ -608,19 +608,19 @@ export class MediaService extends BaseService { const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p'); switch (ffmpegConfig.transcode) { - case TranscodePolicy.DISABLED: { + case TranscodePolicy.Disabled: { return false; } - case TranscodePolicy.ALL: { + case TranscodePolicy.All: { return true; } - case TranscodePolicy.REQUIRED: { + case TranscodePolicy.Required: { return isRequired; } - case TranscodePolicy.OPTIMAL: { + case TranscodePolicy.Optimal: { return isRequired || isLargerThanTargetRes; } - case TranscodePolicy.BITRATE: { + case TranscodePolicy.Bitrate: { return isRequired || isLargerThanTargetBitrate; } default: { @@ -630,12 +630,12 @@ export class MediaService extends BaseService { } private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { - if (ffmpegConfig.transcode === TranscodePolicy.DISABLED) { + if (ffmpegConfig.transcode === TranscodePolicy.Disabled) { return false; } - const name = formatLongName === 'QuickTime / MOV' ? VideoContainer.MOV : (formatName as VideoContainer); - return name !== VideoContainer.MP4 && !ffmpegConfig.acceptedContainers.includes(name); + const name = formatLongName === 'QuickTime / MOV' ? VideoContainer.Mov : (formatName as VideoContainer); + return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name); } isSRGB({ colorspace, profileDescription, bitsPerSample }: Exif): boolean { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index b0ea697edb..7bf9deab4b 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -12,7 +12,7 @@ const DAYS = 3; @Injectable() export class MemoryService extends BaseService { - @OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.MemoryGenerate, queue: QueueName.BackgroundTask }) async onMemoriesCreate() { const users = await this.userRepository.getList({ withDeleted: false }); const usersIds = await Promise.all( @@ -26,7 +26,7 @@ export class MemoryService extends BaseService { ); await this.databaseRepository.withLock(DatabaseLock.MemoryCreation, async () => { - const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE); + const state = await this.systemMetadataRepository.get(SystemMetadataKey.MemoriesState); const start = DateTime.utc().startOf('day').minus({ days: DAYS }); const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start; @@ -43,7 +43,7 @@ export class MemoryService extends BaseService { this.logger.error(`Failed to create memories for ${target.toISO()}`, error); } // update system metadata even when there is an error to minimize the chance of duplicates - await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, { + await this.systemMetadataRepository.set(SystemMetadataKey.MemoriesState, { ...state, lastOnThisDayDate: target.toISO(), }); @@ -60,7 +60,7 @@ export class MemoryService extends BaseService { this.memoryRepository.create( { ownerId, - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year }, memoryAt: target.set({ year }).toISO()!, showAt, @@ -72,7 +72,7 @@ export class MemoryService extends BaseService { ); } - @OnJob({ name: JobName.MEMORIES_CLEANUP, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.MemoryCleanup, queue: QueueName.BackgroundTask }) async onMemoriesCleanup() { await this.memoryRepository.cleanup(); } @@ -87,7 +87,7 @@ export class MemoryService extends BaseService { } async get(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.MemoryRead, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory, auth); } @@ -98,7 +98,7 @@ export class MemoryService extends BaseService { const assetIds = dto.assetIds || []; const allowedAssetIds = await this.checkAccess({ auth, - permission: Permission.ASSET_SHARE, + permission: Permission.AssetShare, ids: assetIds, }); const memory = await this.memoryRepository.create( @@ -117,7 +117,7 @@ export class MemoryService extends BaseService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.MemoryUpdate, ids: [id] }); const memory = await this.memoryRepository.update(id, { isSaved: dto.isSaved, @@ -129,12 +129,12 @@ export class MemoryService extends BaseService { } async remove(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.MEMORY_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.MemoryDelete, ids: [id] }); await this.memoryRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.MemoryRead, ids: [id] }); const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); @@ -148,13 +148,13 @@ export class MemoryService extends BaseService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.MemoryUpdate, ids: [id] }); const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, - canAlwaysRemove: Permission.MEMORY_DELETE, + canAlwaysRemove: Permission.MemoryDelete, }); const hasSuccess = results.find(({ success }) => success); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 881f25d5dd..cc0956b9a8 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -50,7 +50,7 @@ describe(MetadataService.name, () => { mockReadTags(); - mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); delete process.env.TZ; }); @@ -102,11 +102,11 @@ describe(MetadataService.name, () => { it('should queue metadata extraction for all assets without exif values', async () => { mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); - await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.METADATA_EXTRACTION, + name: JobName.AssetExtractMetadata, data: { id: assetStub.image.id }, }, ]); @@ -115,11 +115,11 @@ describe(MetadataService.name, () => { it('should queue metadata extraction for all assets', async () => { mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); - await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.METADATA_EXTRACTION, + name: JobName.AssetExtractMetadata, data: { id: assetStub.image.id }, }, ]); @@ -506,7 +506,7 @@ describe(MetadataService.name, () => { it('should not apply motion photos if asset is video', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -516,7 +516,7 @@ describe(MetadataService.name, () => { expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith( - expect.objectContaining({ assetType: AssetType.VIDEO, visibility: AssetVisibility.HIDDEN }), + expect.objectContaining({ assetType: AssetType.Video, visibility: AssetVisibility.Hidden }), ); }); @@ -583,13 +583,13 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, id: fileStub.livePhotoMotion.uuid, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: 'upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4', + originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, - type: AssetType.VIDEO, + type: AssetType.Video, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); @@ -599,7 +599,7 @@ describe(MetadataService.name, () => { }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ - name: JobName.VIDEO_CONVERSION, + name: JobName.AssetEncodeVideo, data: { id: assetStub.livePhotoMotionAsset.id }, }); }); @@ -641,13 +641,13 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, id: fileStub.livePhotoMotion.uuid, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: 'upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4', + originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, - type: AssetType.VIDEO, + type: AssetType.Video, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); @@ -657,7 +657,7 @@ describe(MetadataService.name, () => { }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ - name: JobName.VIDEO_CONVERSION, + name: JobName.AssetEncodeVideo, data: { id: assetStub.livePhotoMotionAsset.id }, }); }); @@ -699,13 +699,13 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, id: fileStub.livePhotoMotion.uuid, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, originalFileName: 'asset_1.mp4', - originalPath: 'upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4', + originalPath: expect.stringContaining('upload/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, - type: AssetType.VIDEO, + type: AssetType.Video, }); expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); @@ -715,7 +715,7 @@ describe(MetadataService.name, () => { }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ - name: JobName.VIDEO_CONVERSION, + name: JobName.AssetEncodeVideo, data: { id: assetStub.livePhotoMotionAsset.id }, }); }); @@ -737,7 +737,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); expect(mocks.job.queue).toHaveBeenNthCalledWith(1, { - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true }, }); }); @@ -778,7 +778,7 @@ describe(MetadataService.name, () => { mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); mocks.asset.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); @@ -786,7 +786,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoStillAsset.id, @@ -1106,7 +1106,7 @@ describe(MetadataService.name, () => { boundingBoxX2: 200, boundingBoxY1: 20, boundingBoxY2: 60, - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, }, ], [], @@ -1116,7 +1116,7 @@ describe(MetadataService.name, () => { ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.withName.id }, }, ]); @@ -1145,7 +1145,7 @@ describe(MetadataService.name, () => { boundingBoxX2: 200, boundingBoxY1: 20, boundingBoxY2: 60, - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, }, ], [], @@ -1234,7 +1234,7 @@ describe(MetadataService.name, () => { boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2, - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, }, ], [], @@ -1244,7 +1244,7 @@ describe(MetadataService.name, () => { ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.withName.id }, }, ]); @@ -1308,7 +1308,7 @@ describe(MetadataService.name, () => { expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith( - expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), + expect.objectContaining({ visibility: AssetVisibility.Hidden }), ); expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled(); }); @@ -1326,10 +1326,10 @@ describe(MetadataService.name, () => { ownerId: assetStub.livePhotoMotionAsset.ownerId, otherAssetId: assetStub.livePhotoMotionAsset.id, libraryId: null, - type: AssetType.IMAGE, + type: AssetType.Image, }); expect(mocks.asset.update).not.toHaveBeenCalledWith( - expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), + expect.objectContaining({ visibility: AssetVisibility.Hidden }), ); expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled(); }); @@ -1346,7 +1346,7 @@ describe(MetadataService.name, () => { livePhotoCID: 'CID', ownerId: assetStub.livePhotoStillAsset.ownerId, otherAssetId: assetStub.livePhotoStillAsset.id, - type: AssetType.VIDEO, + type: AssetType.Video, }); expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoStillAsset.id, @@ -1354,7 +1354,7 @@ describe(MetadataService.name, () => { }); expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); }); @@ -1457,7 +1457,7 @@ describe(MetadataService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SIDECAR_SYNC, + name: JobName.SidecarSync, data: { id: assetStub.sidecar.id }, }, ]); @@ -1471,7 +1471,7 @@ describe(MetadataService.name, () => { expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.SIDECAR_DISCOVERY, + name: JobName.SidecarDiscovery, data: { id: assetStub.image.id }, }, ]); @@ -1481,13 +1481,13 @@ describe(MetadataService.name, () => { describe('handleSidecarSync', () => { it('should do nothing if asset could not be found', async () => { mocks.asset.getByIds.mockResolvedValue([]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should do nothing if asset has no sidecar path', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -1495,7 +1495,7 @@ describe(MetadataService.name, () => { mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); mocks.storage.checkFileExists.mockResolvedValue(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); expect(mocks.storage.checkFileExists).toHaveBeenCalledWith( `${assetStub.sidecar.originalPath}.xmp`, constants.R_OK, @@ -1511,7 +1511,7 @@ describe(MetadataService.name, () => { mocks.storage.checkFileExists.mockResolvedValueOnce(false); mocks.storage.checkFileExists.mockResolvedValueOnce(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.Success); expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith( 2, assetStub.sidecarWithoutExt.sidecarPath, @@ -1528,7 +1528,7 @@ describe(MetadataService.name, () => { mocks.storage.checkFileExists.mockResolvedValueOnce(true); mocks.storage.checkFileExists.mockResolvedValueOnce(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK); expect(mocks.storage.checkFileExists).toHaveBeenNthCalledWith( 2, @@ -1545,7 +1545,7 @@ describe(MetadataService.name, () => { mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]); mocks.storage.checkFileExists.mockResolvedValue(false); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.Success); expect(mocks.storage.checkFileExists).toHaveBeenCalledWith( `${assetStub.sidecar.originalPath}.xmp`, constants.R_OK, @@ -1603,14 +1603,14 @@ describe(MetadataService.name, () => { describe('handleSidecarWrite', () => { it('should skip assets that do not exist anymore', async () => { mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(void 0); - await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.Failed); expect(mocks.metadata.writeTags).not.toHaveBeenCalled(); }); it('should skip jobs with no metadata', async () => { const asset = factory.jobAssets.sidecarWrite(); mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); - await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Skipped); expect(mocks.metadata.writeTags).not.toHaveBeenCalled(); }); @@ -1629,7 +1629,7 @@ describe(MetadataService.name, () => { longitude: gps, dateTimeOriginal: date, }), - ).resolves.toBe(JobStatus.SUCCESS); + ).resolves.toBe(JobStatus.Success); expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.sidecarPath, { Description: description, ImageDescription: description, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index ea3f810fa4..32a3d98f4e 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -126,7 +126,7 @@ type Dates = { @Injectable() export class MetadataService extends BaseService { - @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.MICROSERVICES] }) + @OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Microservices] }) async onBootstrap() { this.logger.log('Bootstrapping metadata service'); await this.init(); @@ -137,12 +137,12 @@ export class MetadataService extends BaseService { await this.metadataRepository.teardown(); } - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] }) + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) { this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency); } - @OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true }) + @OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.Microservices], server: true }) onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) { this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency); } @@ -151,9 +151,9 @@ export class MetadataService extends BaseService { this.logger.log('Initializing metadata service'); try { - await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); + await this.jobRepository.pause(QueueName.MetadataExtraction); await this.databaseRepository.withLock(DatabaseLock.GeodataImport, () => this.mapRepository.init()); - await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); + await this.jobRepository.resume(QueueName.MetadataExtraction); this.logger.log(`Initialized local reverse geocoder`); } catch (error: Error | any) { @@ -170,7 +170,7 @@ export class MetadataService extends BaseService { return; } - const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO; + const otherType = asset.type === AssetType.Video ? AssetType.Image : AssetType.Video; const match = await this.assetRepository.findLivePhotoMatch({ livePhotoCID: exifInfo.livePhotoCID, ownerId: asset.ownerId, @@ -183,23 +183,23 @@ export class MetadataService extends BaseService { return; } - const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; + const [photoAsset, motionAsset] = asset.type === AssetType.Image ? [asset, match] : [match, asset]; await Promise.all([ this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }), - this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }), + this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.Hidden }), this.albumRepository.removeAssetsFromAll([motionAsset.id]), ]); await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId }); } - @OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION }) - async handleQueueMetadataExtraction(job: JobOf): Promise { + @OnJob({ name: JobName.AssetExtractMetadataQueueAll, queue: QueueName.MetadataExtraction }) + async handleQueueMetadataExtraction(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.METADATA_EXTRACTION; data: { id: string } }[] = []; + let queue: { name: JobName.AssetExtractMetadata; data: { id: string } }[] = []; for await (const asset of this.assetJobRepository.streamForMetadataExtraction(force)) { - queue.push({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id } }); + queue.push({ name: JobName.AssetExtractMetadata, data: { id: asset.id } }); if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(queue); @@ -208,11 +208,11 @@ export class MetadataService extends BaseService { } await this.jobRepository.queueAll(queue); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION }) - async handleMetadataExtraction(data: JobOf) { + @OnJob({ name: JobName.AssetExtractMetadata, queue: QueueName.MetadataExtraction }) + async handleMetadataExtraction(data: JobOf) { const [{ metadata, reverseGeocoding }, asset] = await Promise.all([ this.getConfig({ withCache: true }), this.assetJobRepository.getForMetadataExtraction(data.id), @@ -320,8 +320,8 @@ export class MetadataService extends BaseService { }); } - @OnJob({ name: JobName.QUEUE_SIDECAR, queue: QueueName.SIDECAR }) - async handleQueueSidecar({ force }: JobOf): Promise { + @OnJob({ name: JobName.SidecarQueueAll, queue: QueueName.Sidecar }) + async handleQueueSidecar({ force }: JobOf): Promise { let jobs: JobItem[] = []; const queueAll = async () => { await this.jobRepository.queueAll(jobs); @@ -330,7 +330,7 @@ export class MetadataService extends BaseService { const assets = this.assetJobRepository.streamForSidecar(force); for await (const asset of assets) { - jobs.push({ name: force ? JobName.SIDECAR_SYNC : JobName.SIDECAR_DISCOVERY, data: { id: asset.id } }); + jobs.push({ name: force ? JobName.SidecarSync : JobName.SidecarDiscovery, data: { id: asset.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -338,35 +338,35 @@ export class MetadataService extends BaseService { await queueAll(); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.SIDECAR_SYNC, queue: QueueName.SIDECAR }) - handleSidecarSync({ id }: JobOf): Promise { + @OnJob({ name: JobName.SidecarSync, queue: QueueName.Sidecar }) + handleSidecarSync({ id }: JobOf): Promise { return this.processSidecar(id, true); } - @OnJob({ name: JobName.SIDECAR_DISCOVERY, queue: QueueName.SIDECAR }) - handleSidecarDiscovery({ id }: JobOf): Promise { + @OnJob({ name: JobName.SidecarDiscovery, queue: QueueName.Sidecar }) + handleSidecarDiscovery({ id }: JobOf): Promise { return this.processSidecar(id, false); } @OnEvent({ name: 'AssetTag' }) async handleTagAsset({ assetId }: ArgOf<'AssetTag'>) { - await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); } @OnEvent({ name: 'AssetUntag' }) async handleUntagAsset({ assetId }: ArgOf<'AssetUntag'>) { - await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); } - @OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR }) - async handleSidecarWrite(job: JobOf): Promise { + @OnJob({ name: JobName.SidecarWrite, queue: QueueName.Sidecar }) + async handleSidecarWrite(job: JobOf): Promise { const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job; const asset = await this.assetJobRepository.getForSidecarWriteJob(id); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } const tagsList = (asset.tags || []).map((tag) => tag.value); @@ -386,7 +386,7 @@ export class MetadataService extends BaseService { ); if (Object.keys(exif).length === 0) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } await this.metadataRepository.writeTags(sidecarPath, exif); @@ -395,7 +395,7 @@ export class MetadataService extends BaseService { await this.assetRepository.update({ id, sidecarPath }); } - return JobStatus.SUCCESS; + return JobStatus.Success; } private getImageDimensions(exifTags: ImmichTags): { width?: number; height?: number } { @@ -416,7 +416,7 @@ export class MetadataService extends BaseService { sidecarPath: string | null; type: AssetType; }): Promise { - if (!asset.sidecarPath && asset.type === AssetType.IMAGE) { + if (!asset.sidecarPath && asset.type === AssetType.Image) { return this.metadataRepository.readTags(asset.originalPath); } @@ -431,7 +431,7 @@ export class MetadataService extends BaseService { const [mediaTags, sidecarTags, videoTags] = await Promise.all([ this.metadataRepository.readTags(asset.originalPath), asset.sidecarPath ? this.metadataRepository.readTags(asset.sidecarPath) : null, - asset.type === AssetType.VIDEO ? this.getVideoTags(asset.originalPath) : null, + asset.type === AssetType.Video ? this.getVideoTags(asset.originalPath) : null, ]); // prefer dates from sidecar tags @@ -488,7 +488,7 @@ export class MetadataService extends BaseService { } private isMotionPhoto(asset: { type: AssetType }, tags: ImmichTags): boolean { - return asset.type === AssetType.IMAGE && !!(tags.MotionPhoto || tags.MicroVideo); + return asset.type === AssetType.Image && !!(tags.MotionPhoto || tags.MicroVideo); } private async applyMotionPhotos(asset: Asset, tags: ImmichTags, dates: Dates, stats: Stats) { @@ -558,10 +558,10 @@ export class MetadataService extends BaseService { }); // Hide the motion photo video asset if it's not already hidden to prepare for linking - if (motionAsset.visibility === AssetVisibility.TIMELINE) { + if (motionAsset.visibility === AssetVisibility.Timeline) { await this.assetRepository.update({ id: motionAsset.id, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, }); this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); } @@ -570,7 +570,7 @@ export class MetadataService extends BaseService { motionAsset = await this.assetRepository.create({ id: motionAssetId, libraryId: asset.libraryId, - type: AssetType.VIDEO, + type: AssetType.Video, fileCreatedAt: dates.dateTimeOriginal, fileModifiedAt: stats.mtime, localDateTime: dates.localDateTime, @@ -578,7 +578,7 @@ export class MetadataService extends BaseService { ownerId: asset.ownerId, originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), originalFileName: `${path.parse(asset.originalFileName).name}.mp4`, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, deviceAssetId: 'NONE', deviceId: 'NONE', }); @@ -597,7 +597,7 @@ export class MetadataService extends BaseService { // note asset.livePhotoVideoId is not motionAsset.id yet if (asset.livePhotoVideoId) { await this.jobRepository.queue({ - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: asset.livePhotoVideoId, deleteOnDisk: true }, }); this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`); @@ -612,7 +612,7 @@ export class MetadataService extends BaseService { this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`); await this.handleMetadataExtraction({ id: motionAsset.id }); - await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: motionAsset.id } }); + await this.jobRepository.queue({ name: JobName.AssetEncodeVideo, data: { id: motionAsset.id } }); } this.logger.debug(`Finished motion photo video extraction for asset ${asset.id}: ${asset.originalPath}`); @@ -740,7 +740,7 @@ export class MetadataService extends BaseService { boundingBoxY1: Math.floor((region.Area.Y - region.Area.H / 2) * imageHeight), boundingBoxX2: Math.floor((region.Area.X + region.Area.W / 2) * imageWidth), boundingBoxY2: Math.floor((region.Area.Y + region.Area.H / 2) * imageHeight), - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, }; facesToAdd.push(face); @@ -753,11 +753,11 @@ export class MetadataService extends BaseService { if (missing.length > 0) { this.logger.debugFn(() => `Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`); const newPersonIds = await this.personRepository.createAll(missing); - const jobs = newPersonIds.map((id) => ({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }) as const); + const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const); await this.jobRepository.queueAll(jobs); } - const facesToRemove = asset.faces.filter((face) => face.sourceType === SourceType.EXIF).map((face) => face.id); + const facesToRemove = asset.faces.filter((face) => face.sourceType === SourceType.Exif).map((face) => face.id); if (facesToRemove.length > 0) { this.logger.debug(`Removing ${facesToRemove.length} faces for asset ${asset.id}: ${asset.originalPath}`); } @@ -894,15 +894,15 @@ export class MetadataService extends BaseService { const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } if (isSync && !asset.sidecarPath) { - return JobStatus.FAILED; + return JobStatus.Failed; } - if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) { - return JobStatus.FAILED; + if (!isSync && (asset.visibility === AssetVisibility.Hidden || asset.sidecarPath) && !asset.isExternal) { + return JobStatus.Failed; } // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp @@ -927,22 +927,22 @@ export class MetadataService extends BaseService { if (sidecarPath !== asset.sidecarPath) { await this.assetRepository.update({ id: asset.id, sidecarPath }); } - return JobStatus.SUCCESS; + return JobStatus.Success; } if (sidecarPath) { this.logger.debug(`Detected sidecar at '${sidecarPath}' for asset ${asset.id}: ${asset.originalPath}`); await this.assetRepository.update({ id: asset.id, sidecarPath }); - return JobStatus.SUCCESS; + return JobStatus.Success; } if (!isSync) { - return JobStatus.FAILED; + return JobStatus.Failed; } this.logger.debug(`No sidecar found for asset ${asset.id}: ${asset.originalPath}`); await this.assetRepository.update({ id: asset.id, sidecarPath: null }); - return JobStatus.SUCCESS; + return JobStatus.Success; } } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index bca7074194..eef1c4f8b2 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -131,7 +131,7 @@ describe(NotificationService.name, () => { it('should queue the generate thumbnail job', async () => { await sut.onAssetShow({ assetId: 'asset-id', userId: 'user-id' }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.GENERATE_THUMBNAILS, + name: JobName.AssetGenerateThumbnails, data: { id: 'asset-id', notify: true }, }); }); @@ -146,7 +146,7 @@ describe(NotificationService.name, () => { it('should queue notify signup event if notify is true', async () => { await sut.onUserSignup({ id: '', notify: true }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.NOTIFY_SIGNUP, + name: JobName.NotifyUserSignup, data: { id: '', tempPassword: undefined }, }); }); @@ -156,7 +156,7 @@ describe(NotificationService.name, () => { it('should queue notify album update event', async () => { await sut.onAlbumUpdate({ id: 'album', recipientId: '42' }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.NOTIFY_ALBUM_UPDATE, + name: JobName.NotifyAlbumUpdate, data: { id: 'album', recipientId: '42', delay: 300_000 }, }); }); @@ -166,7 +166,7 @@ describe(NotificationService.name, () => { it('should queue notify album invite event', async () => { await sut.onAlbumInvite({ id: '', userId: '42' }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.NOTIFY_ALBUM_INVITE, + name: JobName.NotifyAlbumInvite, data: { id: '', recipientId: '42' }, }); }); @@ -242,7 +242,7 @@ describe(NotificationService.name, () => { describe('handleUserSignup', () => { it('should skip if user could not be found', async () => { - await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.Skipped); }); it('should be successful', async () => { @@ -250,9 +250,9 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.Success); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: expect.objectContaining({ subject: 'Welcome to Immich' }), }); }); @@ -260,14 +260,14 @@ describe(NotificationService.name, () => { describe('handleAlbumInvite', () => { it('should skip if album could not be found', async () => { - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); expect(mocks.user.get).not.toHaveBeenCalled(); }); it('should skip if recipient could not be found', async () => { mocks.album.getById.mockResolvedValue(albumStub.empty); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); }); @@ -277,13 +277,13 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: false, albumInvite: true } }, }, ], }); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); }); it('should skip if the recipient has email notifications for album invite disabled', async () => { @@ -292,13 +292,13 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumInvite: false } }, }, ], }); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); }); it('should send invite email', async () => { @@ -307,7 +307,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumInvite: true } }, }, ], @@ -315,9 +315,9 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album') }), }); }); @@ -328,7 +328,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumInvite: true } }, }, ], @@ -337,13 +337,13 @@ describe(NotificationService.name, () => { mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, - AssetFileType.THUMBNAIL, + AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album'), imageAttachments: undefined, @@ -357,7 +357,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumInvite: true } }, }, ], @@ -365,16 +365,16 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ - { id: '1', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' }, + { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' }, ]); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, - AssetFileType.THUMBNAIL, + AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album'), imageAttachments: [{ filename: 'album-thumbnail.jpg', path: expect.anything(), cid: expect.anything() }], @@ -388,7 +388,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumInvite: true } }, }, ], @@ -397,13 +397,13 @@ describe(NotificationService.name, () => { mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); - await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, - AssetFileType.THUMBNAIL, + AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album'), imageAttachments: [{ filename: 'album-thumbnail.ext', path: expect.anything(), cid: expect.anything() }], @@ -414,14 +414,14 @@ describe(NotificationService.name, () => { describe('handleAlbumUpdate', () => { it('should skip if album could not be found', async () => { - await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.user.get).not.toHaveBeenCalled(); }); it('should skip if owner could not be found', async () => { mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); - await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); @@ -448,7 +448,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: false, albumUpdate: true } }, }, ], @@ -470,7 +470,7 @@ describe(NotificationService.name, () => { ...userStub.user1, metadata: [ { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: { emailNotifications: { enabled: true, albumUpdate: false } }, }, ], @@ -500,9 +500,9 @@ describe(NotificationService.name, () => { it('should add new recipients for new images if job is already queued', async () => { await sut.onAlbumUpdate({ id: '1', recipientId: '2' } as INotifyAlbumUpdateJob); - expect(mocks.job.removeJob).toHaveBeenCalledWith(JobName.NOTIFY_ALBUM_UPDATE, '1/2'); + expect(mocks.job.removeJob).toHaveBeenCalledWith(JobName.NotifyAlbumUpdate, '1/2'); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.NOTIFY_ALBUM_UPDATE, + name: JobName.NotifyAlbumUpdate, data: { id: '1', delay: 300_000, @@ -515,7 +515,7 @@ describe(NotificationService.name, () => { describe('handleSendEmail', () => { it('should skip if smtp notifications are disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: false } } }); - await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.Skipped); }); it('should send mail successfully', async () => { @@ -524,7 +524,7 @@ describe(NotificationService.name, () => { }); mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); - await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.Success); expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'test@immich.app' })); }); @@ -534,7 +534,7 @@ describe(NotificationService.name, () => { }); mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); - await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.Success); expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'demo@immich.app' })); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 80a20195a1..1a257309b2 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -39,19 +39,19 @@ export class NotificationService extends BaseService { } async updateAll(auth: AuthDto, dto: NotificationUpdateAllDto) { - await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_UPDATE }); + await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NotificationUpdate }); await this.notificationRepository.updateAll(dto.ids, { readAt: dto.readAt, }); } async deleteAll(auth: AuthDto, dto: NotificationDeleteAllDto) { - await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_DELETE }); + await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NotificationDelete }); await this.notificationRepository.deleteAll(dto.ids); } async get(auth: AuthDto, id: string) { - await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_READ }); + await this.requireAccess({ auth, ids: [id], permission: Permission.NotificationRead }); const item = await this.notificationRepository.get(id); if (!item) { throw new BadRequestException('Notification not found'); @@ -60,7 +60,7 @@ export class NotificationService extends BaseService { } async update(auth: AuthDto, id: string, dto: NotificationUpdateDto) { - await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_UPDATE }); + await this.requireAccess({ auth, ids: [id], permission: Permission.NotificationUpdate }); const item = await this.notificationRepository.update(id, { readAt: dto.readAt, }); @@ -68,11 +68,11 @@ export class NotificationService extends BaseService { } async delete(auth: AuthDto, id: string) { - await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_DELETE }); + await this.requireAccess({ auth, ids: [id], permission: Permission.NotificationDelete }); await this.notificationRepository.delete(id); } - @OnJob({ name: JobName.NOTIFICATIONS_CLEANUP, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.NotificationsCleanup, queue: QueueName.BackgroundTask }) async onNotificationsCleanup() { await this.notificationRepository.cleanup(); } @@ -87,7 +87,7 @@ export class NotificationService extends BaseService { this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data)); switch (job.name) { - case JobName.BACKUP_DATABASE: { + case JobName.DatabaseBackup: { const errorMessage = error instanceof Error ? error.message : error; const item = await this.notificationRepository.create({ userId: admin.id, @@ -135,7 +135,7 @@ export class NotificationService extends BaseService { @OnEvent({ name: 'AssetShow' }) async onAssetShow({ assetId }: ArgOf<'AssetShow'>) { - await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id: assetId, notify: true } }); + await this.jobRepository.queue({ name: JobName.AssetGenerateThumbnails, data: { id: assetId, notify: true } }); } @OnEvent({ name: 'AssetTrash' }) @@ -193,22 +193,22 @@ export class NotificationService extends BaseService { @OnEvent({ name: 'UserSignup' }) async onUserSignup({ notify, id, tempPassword }: ArgOf<'UserSignup'>) { if (notify) { - await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } }); + await this.jobRepository.queue({ name: JobName.NotifyUserSignup, data: { id, tempPassword } }); } } @OnEvent({ name: 'AlbumUpdate' }) async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) { - await this.jobRepository.removeJob(JobName.NOTIFY_ALBUM_UPDATE, `${id}/${recipientId}`); + await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`); await this.jobRepository.queue({ - name: JobName.NOTIFY_ALBUM_UPDATE, + name: JobName.NotifyAlbumUpdate, data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs }, }); } @OnEvent({ name: 'AlbumInvite' }) async onAlbumInvite({ id, userId }: ArgOf<'AlbumInvite'>) { - await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } }); + await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId } }); } @OnEvent({ name: 'SessionDelete' }) @@ -313,11 +313,11 @@ export class NotificationService extends BaseService { return { name, html: templateResponse }; } - @OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION }) - async handleUserSignup({ id, tempPassword }: JobOf) { + @OnJob({ name: JobName.NotifyUserSignup, queue: QueueName.Notification }) + async handleUserSignup({ id, tempPassword }: JobOf) { const user = await this.userRepository.get(id, { withDeleted: false }); if (!user) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { server, templates } = await this.getConfig({ withCache: true }); @@ -333,7 +333,7 @@ export class NotificationService extends BaseService { }); await this.jobRepository.queue({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: { to: user.email, subject: 'Welcome to Immich', @@ -342,25 +342,25 @@ export class NotificationService extends BaseService { }, }); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.NOTIFY_ALBUM_INVITE, queue: QueueName.NOTIFICATION }) - async handleAlbumInvite({ id, recipientId }: JobOf) { + @OnJob({ name: JobName.NotifyAlbumInvite, queue: QueueName.Notification }) + async handleAlbumInvite({ id, recipientId }: JobOf) { const album = await this.albumRepository.getById(id, { withAssets: false }); if (!album) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const recipient = await this.userRepository.get(recipientId, { withDeleted: false }); if (!recipient) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { emailNotifications } = getPreferences(recipient.metadata); if (!emailNotifications.enabled || !emailNotifications.albumInvite) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const attachment = await this.getAlbumThumbnailAttachment(album); @@ -380,7 +380,7 @@ export class NotificationService extends BaseService { }); await this.jobRepository.queue({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: { to: recipient.email, subject: `You have been added to a shared album - ${album.albumName}`, @@ -390,20 +390,20 @@ export class NotificationService extends BaseService { }, }); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.NOTIFY_ALBUM_UPDATE, queue: QueueName.NOTIFICATION }) - async handleAlbumUpdate({ id, recipientId }: JobOf) { + @OnJob({ name: JobName.NotifyAlbumUpdate, queue: QueueName.Notification }) + async handleAlbumUpdate({ id, recipientId }: JobOf) { const album = await this.albumRepository.getById(id, { withAssets: false }); if (!album) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const owner = await this.userRepository.get(album.ownerId, { withDeleted: false }); if (!owner) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const attachment = await this.getAlbumThumbnailAttachment(album); @@ -412,13 +412,13 @@ export class NotificationService extends BaseService { const user = await this.userRepository.get(recipientId, { withDeleted: false }); if (!user) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { emailNotifications } = getPreferences(user.metadata); if (!emailNotifications.enabled || !emailNotifications.albumUpdate) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { html, text } = await this.emailRepository.renderEmail({ @@ -434,7 +434,7 @@ export class NotificationService extends BaseService { }); await this.jobRepository.queue({ - name: JobName.SEND_EMAIL, + name: JobName.SendMail, data: { to: user.email, subject: `New media has been added to an album - ${album.albumName}`, @@ -444,14 +444,14 @@ export class NotificationService extends BaseService { }, }); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.SEND_EMAIL, queue: QueueName.NOTIFICATION }) - async handleSendEmail(data: JobOf): Promise { + @OnJob({ name: JobName.SendMail, queue: QueueName.Notification }) + async handleSendEmail(data: JobOf): Promise { const { notifications } = await this.getConfig({ withCache: false }); if (!notifications.smtp.enabled) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const { to, subject, html, text: plain } = data; @@ -468,7 +468,7 @@ export class NotificationService extends BaseService { this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async getAlbumThumbnailAttachment(album: { @@ -480,7 +480,7 @@ export class NotificationService extends BaseService { const albumThumbnailFiles = await this.assetJobRepository.getAlbumThumbnailFiles( album.albumThumbnailAssetId, - AssetFileType.THUMBNAIL, + AssetFileType.Thumbnail, ); if (albumThumbnailFiles.length !== 1) { diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index 3723634948..755b688397 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -40,7 +40,7 @@ export class PartnerService extends BaseService { } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); + await this.requireAccess({ auth, permission: Permission.PartnerUpdate, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.partnerRepository.update(partnerId, { inTimeline: dto.inTimeline }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index d9df2225f4..13c3128317 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -182,7 +182,7 @@ describe(PersonService.name, () => { new ImmichFileResponse({ path: '/path/to/thumbnail.jpg', contentType: 'image/jpeg', - cacheControl: CacheControl.PRIVATE_WITHOUT_CACHE, + cacheControl: CacheControl.PrivateWithoutCache, }), ); expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); @@ -276,7 +276,7 @@ describe(PersonService.name, () => { }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: 'person-1' }, }); expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); @@ -337,7 +337,7 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.newThumbnail.id }, }, ]); @@ -346,7 +346,7 @@ describe(PersonService.name, () => { describe('handlePersonMigration', () => { it('should not move person files', async () => { - await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.FAILED); + await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.Failed); }); }); @@ -373,7 +373,7 @@ describe(PersonService.name, () => { await sut.createNewFeaturePhoto([personStub.newThumbnail.id]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PERSON_THUMBNAIL, + name: JobName.PersonGenerateThumbnail, data: { id: personStub.newThumbnail.id }, }, ]); @@ -447,7 +447,7 @@ describe(PersonService.name, () => { it('should skip if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -462,7 +462,7 @@ describe(PersonService.name, () => { expect(mocks.person.vacuum).not.toHaveBeenCalled(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACE_DETECTION, + name: JobName.AssetDetectFaces, data: { id: assetStub.image.id }, }, ]); @@ -474,14 +474,14 @@ describe(PersonService.name, () => { await sut.handleQueueDetectFaces({ force: true }); - expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); + expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning }); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACE_DETECTION, + name: JobName.AssetDetectFaces, data: { id: assetStub.image.id }, }, ]); @@ -499,11 +499,11 @@ describe(PersonService.name, () => { expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACE_DETECTION, + name: JobName.AssetDetectFaces, data: { id: assetStub.image.id }, }, ]); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PERSON_CLEANUP }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonCleanup }); }); it('should delete existing people and faces if forced', async () => { @@ -518,7 +518,7 @@ describe(PersonService.name, () => { expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACE_DETECTION, + name: JobName.AssetDetectFaces, data: { id: assetStub.image.id }, }, ]); @@ -540,7 +540,7 @@ describe(PersonService.name, () => { }); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); @@ -556,7 +556,7 @@ describe(PersonService.name, () => { delayed: 0, }); - await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); @@ -577,15 +577,15 @@ describe(PersonService.name, () => { expect(mocks.person.getAllFaces).toHaveBeenCalledWith({ personId: null, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACIAL_RECOGNITION, + name: JobName.FacialRecognition, data: { id: faceStub.face1.id, deferred: false }, }, ]); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { lastRun: expect.any(String), }); expect(mocks.person.vacuum).not.toHaveBeenCalled(); @@ -609,11 +609,11 @@ describe(PersonService.name, () => { expect(mocks.person.getAllFaces).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACIAL_RECOGNITION, + name: JobName.FacialRecognition, data: { id: faceStub.face1.id, deferred: false }, }, ]); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { lastRun: expect.any(String), }); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); @@ -637,19 +637,19 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({ force: false, nightly: true }); - expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE); + expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState); expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce(); expect(mocks.person.getAllFaces).toHaveBeenCalledWith({ personId: null, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACIAL_RECOGNITION, + name: JobName.FacialRecognition, data: { id: faceStub.face1.id, deferred: false }, }, ]); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { lastRun: expect.any(String), }); expect(mocks.person.vacuum).not.toHaveBeenCalled(); @@ -665,7 +665,7 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); - expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE); + expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState); expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce(); expect(mocks.person.getAllFaces).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); @@ -690,10 +690,10 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({ force: true }); expect(mocks.person.deleteFaces).not.toHaveBeenCalled(); - expect(mocks.person.unassignFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); + expect(mocks.person.unassignFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.FACIAL_RECOGNITION, + name: JobName.FacialRecognition, data: { id: faceStub.face1.id, deferred: false }, }, ]); @@ -711,7 +711,7 @@ describe(PersonService.name, () => { it('should skip if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.Skipped); expect(mocks.asset.getByIds).not.toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); @@ -754,8 +754,8 @@ describe(PersonService.name, () => { expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, - { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, + { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, + { name: JobName.FacialRecognition, data: { id: faceId } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); @@ -790,8 +790,8 @@ describe(PersonService.name, () => { expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, - { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, + { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, + { name: JobName.FacialRecognition, data: { id: faceId } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); @@ -830,8 +830,8 @@ describe(PersonService.name, () => { expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, - { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, + { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, + { name: JobName.FacialRecognition, data: { id: faceId } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); @@ -840,7 +840,7 @@ describe(PersonService.name, () => { describe('handleRecognizeFaces', () => { it('should fail if face does not exist', async () => { - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Failed); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); @@ -850,7 +850,7 @@ describe(PersonService.name, () => { const face = { ...faceStub.face1, asset: null }; mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(face); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Failed); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); @@ -859,7 +859,7 @@ describe(PersonService.name, () => { it('should skip if face already has an assigned person', async () => { mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.face1); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.SKIPPED); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Skipped); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); @@ -1008,7 +1008,7 @@ describe(PersonService.name, () => { await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.FACIAL_RECOGNITION, + name: JobName.FacialRecognition, data: { id: faceStub.noPerson1.id, deferred: true }, }); expect(mocks.search.searchFaces).toHaveBeenCalledTimes(1); @@ -1161,7 +1161,7 @@ describe(PersonService.name, () => { id: faceStub.face1.id, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, person: mapPerson(personStub.withName), }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index af34e6eda9..d0c43c3dad 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -78,7 +78,7 @@ export class PersonService extends BaseService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personId] }); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -86,7 +86,7 @@ export class PersonService extends BaseService { const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); + await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [face.id] }); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -107,8 +107,8 @@ export class PersonService extends BaseService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); - await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personId] }); + await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [dto.id] }); const face = await this.personRepository.getFaceById(dto.id); const person = await this.findOrFail(personId); @@ -124,7 +124,7 @@ export class PersonService extends BaseService { } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [dto.id] }); + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] }); const faces = await this.personRepository.getFaces(dto.id); return faces.map((asset) => mapFaces(asset, auth)); } @@ -140,7 +140,7 @@ export class PersonService extends BaseService { if (assetFace) { await this.personRepository.update({ id: personId, faceAssetId: assetFace.id }); - jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } }); + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: personId } }); } } @@ -148,17 +148,17 @@ export class PersonService extends BaseService { } async getById(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); return this.findOrFail(id).then(mapPerson); } async getStatistics(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); return this.personRepository.getStatistics(id); } async getThumbnail(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); const person = await this.personRepository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); @@ -167,7 +167,7 @@ export class PersonService extends BaseService { return new ImmichFileResponse({ path: person.thumbnailPath, contentType: mimeTypes.lookup(person.thumbnailPath), - cacheControl: CacheControl.PRIVATE_WITHOUT_CACHE, + cacheControl: CacheControl.PrivateWithoutCache, }); } @@ -185,13 +185,13 @@ export class PersonService extends BaseService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] }); const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto; // TODO: set by faceId directly let faceId: string | undefined = undefined; if (assetId) { - await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [assetId] }); + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] }); const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); @@ -211,7 +211,7 @@ export class PersonService extends BaseService { }); if (assetId) { - await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); + await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id } }); } return mapPerson(person); @@ -242,7 +242,7 @@ export class PersonService extends BaseService { } async deleteAll(auth: AuthDto, { ids }: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.PERSON_DELETE, ids }); + await this.requireAccess({ auth, permission: Permission.PersonDelete, ids }); const people = await this.personRepository.getForPeopleDelete(ids); await this.removeAllPeople(people); } @@ -254,22 +254,22 @@ export class PersonService extends BaseService { this.logger.debug(`Deleted ${people.length} people`); } - @OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.PersonCleanup, queue: QueueName.BackgroundTask }) async handlePersonCleanup(): Promise { const people = await this.personRepository.getAllWithoutFaces(); await this.removeAllPeople(people); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.QUEUE_FACE_DETECTION, queue: QueueName.FACE_DETECTION }) - async handleQueueDetectFaces({ force }: JobOf): Promise { + @OnJob({ name: JobName.AssetDetectFacesQueueAll, queue: QueueName.FaceDetection }) + async handleQueueDetectFaces({ force }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: false }); if (!isFacialRecognitionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } if (force) { - await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.personRepository.deleteFaces({ sourceType: SourceType.MachineLearning }); await this.handlePersonCleanup(); await this.personRepository.vacuum({ reindexVectors: true }); } @@ -277,7 +277,7 @@ export class PersonService extends BaseService { let jobs: JobItem[] = []; const assets = this.assetJobRepository.streamForDetectFacesJob(force); for await (const asset of assets) { - jobs.push({ name: JobName.FACE_DETECTION, data: { id: asset.id } }); + jobs.push({ name: JobName.AssetDetectFaces, data: { id: asset.id } }); if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(jobs); @@ -288,27 +288,27 @@ export class PersonService extends BaseService { await this.jobRepository.queueAll(jobs); if (force === undefined) { - await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP }); + await this.jobRepository.queue({ name: JobName.PersonCleanup }); } - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.FACE_DETECTION, queue: QueueName.FACE_DETECTION }) - async handleDetectFaces({ id }: JobOf): Promise { + @OnJob({ name: JobName.AssetDetectFaces, queue: QueueName.FaceDetection }) + async handleDetectFaces({ id }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const asset = await this.assetJobRepository.getForDetectFacesJob(id); const previewFile = asset?.files[0]; if (!asset || asset.files.length !== 1 || !previewFile) { - return JobStatus.FAILED; + return JobStatus.Failed; } - if (asset.visibility === AssetVisibility.HIDDEN) { - return JobStatus.SKIPPED; + if (asset.visibility === AssetVisibility.Hidden) { + return JobStatus.Skipped; } const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( @@ -323,7 +323,7 @@ export class PersonService extends BaseService { const mlFaceIds = new Set(); for (const face of asset.faces) { - if (face.sourceType === SourceType.MACHINE_LEARNING) { + if (face.sourceType === SourceType.MachineLearning) { mlFaceIds.add(face.id); } } @@ -368,15 +368,15 @@ export class PersonService extends BaseService { if (facesToAdd.length > 0) { this.logger.log(`Detected ${facesToAdd.length} new faces in asset ${id}`); - const jobs = facesToAdd.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id } }) as const); - await this.jobRepository.queueAll([{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, ...jobs]); + const jobs = facesToAdd.map((face) => ({ name: JobName.FacialRecognition, data: { id: face.id } }) as const); + await this.jobRepository.queueAll([{ name: JobName.FacialRecognitionQueueAll, data: { force: false } }, ...jobs]); } else if (embeddings.length > 0) { this.logger.log(`Added ${embeddings.length} face embeddings for asset ${id}`); } await this.assetRepository.upsertJobStatus({ assetId: asset.id, facesRecognizedAt: new Date() }); - return JobStatus.SUCCESS; + return JobStatus.Success; } private iou( @@ -396,50 +396,50 @@ export class PersonService extends BaseService { return intersection / union; } - @OnJob({ name: JobName.QUEUE_FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION }) - async handleQueueRecognizeFaces({ force, nightly }: JobOf): Promise { + @OnJob({ name: JobName.FacialRecognitionQueueAll, queue: QueueName.FacialRecognition }) + async handleQueueRecognizeFaces({ force, nightly }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: false }); if (!isFacialRecognitionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } - await this.jobRepository.waitForQueueCompletion(QueueName.THUMBNAIL_GENERATION, QueueName.FACE_DETECTION); + await this.jobRepository.waitForQueueCompletion(QueueName.ThumbnailGeneration, QueueName.FaceDetection); if (nightly) { const [state, latestFaceDate] = await Promise.all([ - this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE), + this.systemMetadataRepository.get(SystemMetadataKey.FacialRecognitionState), this.personRepository.getLatestFaceDate(), ]); if (state?.lastRun && latestFaceDate && state.lastRun > latestFaceDate) { this.logger.debug('Skipping facial recognition nightly since no face has been added since the last run'); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } } - const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION); + const { waiting } = await this.jobRepository.getJobCounts(QueueName.FacialRecognition); if (force) { - await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.personRepository.unassignFaces({ sourceType: SourceType.MachineLearning }); await this.handlePersonCleanup(); await this.personRepository.vacuum({ reindexVectors: false }); } else if (waiting) { this.logger.debug( `Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`, ); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } - await this.databaseRepository.prewarm(VectorIndex.FACE); + await this.databaseRepository.prewarm(VectorIndex.Face); const lastRun = new Date().toISOString(); const facePagination = this.personRepository.getAllFaces( - force ? undefined : { personId: null, sourceType: SourceType.MACHINE_LEARNING }, + force ? undefined : { personId: null, sourceType: SourceType.MachineLearning }, ); - let jobs: { name: JobName.FACIAL_RECOGNITION; data: { id: string; deferred: false } }[] = []; + let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = []; for await (const face of facePagination) { - jobs.push({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id, deferred: false } }); + jobs.push({ name: JobName.FacialRecognition, data: { id: face.id, deferred: false } }); if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(jobs); @@ -449,37 +449,37 @@ export class PersonService extends BaseService { await this.jobRepository.queueAll(jobs); - await this.systemMetadataRepository.set(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun }); + await this.systemMetadataRepository.set(SystemMetadataKey.FacialRecognitionState, { lastRun }); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION }) - async handleRecognizeFaces({ id, deferred }: JobOf): Promise { + @OnJob({ name: JobName.FacialRecognition, queue: QueueName.FacialRecognition }) + async handleRecognizeFaces({ id, deferred }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const face = await this.personRepository.getFaceForFacialRecognitionJob(id); if (!face || !face.asset) { this.logger.warn(`Face ${id} not found`); - return JobStatus.FAILED; + return JobStatus.Failed; } - if (face.sourceType !== SourceType.MACHINE_LEARNING) { + if (face.sourceType !== SourceType.MachineLearning) { this.logger.warn(`Skipping face ${id} due to source ${face.sourceType}`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } if (!face.faceSearch?.embedding) { this.logger.warn(`Face ${id} does not have an embedding`); - return JobStatus.FAILED; + return JobStatus.Failed; } if (face.personId) { this.logger.debug(`Face ${id} already has a person assigned`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const matches = await this.searchRepository.searchFaces({ @@ -493,18 +493,18 @@ export class PersonService extends BaseService { // `matches` also includes the face itself if (machineLearning.facialRecognition.minFaces > 1 && matches.length <= 1) { this.logger.debug(`Face ${id} only matched the face itself, skipping`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } this.logger.debug(`Face ${id} has ${matches.length} matches`); const isCore = matches.length >= machineLearning.facialRecognition.minFaces && - face.asset.visibility === AssetVisibility.TIMELINE; + face.asset.visibility === AssetVisibility.Timeline; if (!isCore && !deferred) { this.logger.debug(`Deferring non-core face ${id} for later processing`); - await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); - return JobStatus.SKIPPED; + await this.jobRepository.queue({ name: JobName.FacialRecognition, data: { id, deferred: true } }); + return JobStatus.Skipped; } let personId = matches.find((match) => match.personId)?.personId; @@ -526,7 +526,7 @@ export class PersonService extends BaseService { if (isCore && !personId) { this.logger.log(`Creating new person for face ${id}`); const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id }); - await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } }); + await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } }); personId = newPerson.id; } @@ -535,19 +535,19 @@ export class PersonService extends BaseService { await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId }); } - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.MIGRATE_PERSON, queue: QueueName.MIGRATION }) - async handlePersonMigration({ id }: JobOf): Promise { + @OnJob({ name: JobName.PersonFileMigration, queue: QueueName.Migration }) + async handlePersonMigration({ id }: JobOf): Promise { const person = await this.personRepository.getById(id); if (!person) { - return JobStatus.FAILED; + return JobStatus.Failed; } - await this.storageCore.movePersonFile(person, PersonPathType.FACE); + await this.storageCore.movePersonFile(person, PersonPathType.Face); - return JobStatus.SUCCESS; + return JobStatus.Success; } async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise { @@ -556,7 +556,7 @@ export class PersonService extends BaseService { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] }); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; @@ -564,7 +564,7 @@ export class PersonService extends BaseService { const allowedIds = await this.checkAccess({ auth, - permission: Permission.PERSON_MERGE, + permission: Permission.PersonMerge, ids: mergeIds, }); @@ -623,8 +623,8 @@ export class PersonService extends BaseService { // TODO return a asset face response async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise { await Promise.all([ - this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [dto.assetId] }), - this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [dto.personId] }), + this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.assetId] }), + this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }), ]); await this.personRepository.createAssetFace({ @@ -636,12 +636,12 @@ export class PersonService extends BaseService { boundingBoxX2: dto.x + dto.width, boundingBoxY1: dto.y, boundingBoxY2: dto.y + dto.height, - sourceType: SourceType.MANUAL, + sourceType: SourceType.Manual, }); } async deleteFace(auth: AuthDto, id: string, dto: AssetFaceDeleteDto): Promise { - await this.requireAccess({ auth, permission: Permission.FACE_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.FaceDelete, ids: [id] }); return dto.force ? this.personRepository.deleteAssetFace(id) : this.personRepository.softDeleteAssetFaces(id); } diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index a10c01e8d3..1c75c4a434 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -46,7 +46,7 @@ export class SearchService extends BaseService { } async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { - if (dto.visibility === AssetVisibility.LOCKED) { + if (dto.visibility === AssetVisibility.Locked) { requireElevatedPermission(auth); } @@ -65,7 +65,7 @@ export class SearchService extends BaseService { ...dto, checksum, userIds, - orderDirection: dto.order ?? AssetOrder.DESC, + orderDirection: dto.order ?? AssetOrder.Desc, }, ); @@ -82,7 +82,7 @@ export class SearchService extends BaseService { } async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { - if (dto.visibility === AssetVisibility.LOCKED) { + if (dto.visibility === AssetVisibility.Locked) { requireElevatedPermission(auth); } @@ -92,7 +92,7 @@ export class SearchService extends BaseService { } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { - if (dto.visibility === AssetVisibility.LOCKED) { + if (dto.visibility === AssetVisibility.Locked) { requireElevatedPermission(auth); } diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 05ebda6a94..06ddd32601 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -28,7 +28,7 @@ describe(ServerService.name, () => { diskUseRaw: 300, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); it('should return the disk space as KiB', async () => { @@ -44,7 +44,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); it('should return the disk space as MiB', async () => { @@ -60,7 +60,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); it('should return the disk space as GiB', async () => { @@ -80,7 +80,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); it('should return the disk space as TiB', async () => { @@ -100,7 +100,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); it('should return the disk space as PiB', async () => { @@ -120,7 +120,7 @@ describe(ServerService.name, () => { diskUseRaw: 300_000_000_000_000_000, }); - expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library'); + expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith(expect.stringContaining('upload/library')); }); }); @@ -256,7 +256,7 @@ describe(ServerService.name, () => { const license = { licenseKey: 'IMSV-license-key', activationKey: 'activation-key' }; await sut.setLicense(license); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.LICENSE, expect.any(Object)); + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.License, expect.any(Object)); }); it('should not save license if invalid', async () => { diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 5ad93b40ef..dae484cce8 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -27,7 +27,7 @@ export class ServerService extends BaseService { async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { - await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + await this.systemMetadataRepository.set(SystemMetadataKey.AdminOnboarding, { isOnboarded: true, }); } @@ -38,7 +38,7 @@ export class ServerService extends BaseService { const version = `v${serverVersion.toString()}`; const { buildMetadata } = this.configRepository.getEnv(); const buildVersions = await this.serverInfoRepository.getBuildVersions(); - const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE); + const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.License); return { version, @@ -60,7 +60,7 @@ export class ServerService extends BaseService { } async getStorage(): Promise { - const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); + const libraryBase = StorageCore.getBaseFolder(StorageFolder.Library); const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase); const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2); @@ -111,7 +111,7 @@ export class ServerService extends BaseService { async getSystemConfig(): Promise { const config = await this.getConfig({ withCache: false }); const isInitialized = await this.userRepository.hasAdmin(); - const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING); + const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { loginPageMessage: config.server.loginPageMessage, @@ -163,11 +163,11 @@ export class ServerService extends BaseService { } async deleteLicense(): Promise { - await this.systemMetadataRepository.delete(SystemMetadataKey.LICENSE); + await this.systemMetadataRepository.delete(SystemMetadataKey.License); } async getLicense(): Promise { - const license = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE); + const license = await this.systemMetadataRepository.get(SystemMetadataKey.License); if (!license) { throw new NotFoundException(); } @@ -186,7 +186,7 @@ export class ServerService extends BaseService { const licenseData = { ...dto, activatedAt: new Date() }; - await this.systemMetadataRepository.set(SystemMetadataKey.LICENSE, licenseData); + await this.systemMetadataRepository.set(SystemMetadataKey.License, licenseData); return licenseData; } diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index 7ac338da80..3cbad28389 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -19,7 +19,7 @@ describe('SessionService', () => { describe('handleCleanup', () => { it('should clean sessions', async () => { mocks.session.cleanup.mockResolvedValue([]); - await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.Success); }); }); diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 198e380c53..a9c7e92fcb 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -14,7 +14,7 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class SessionService extends BaseService { - @OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.SessionCleanup, queue: QueueName.BackgroundTask }) async handleCleanup(): Promise { const sessions = await this.sessionRepository.cleanup(); for (const session of sessions) { @@ -23,7 +23,7 @@ export class SessionService extends BaseService { this.logger.log(`Deleted ${sessions.length} expired session tokens`); - return JobStatus.SUCCESS; + return JobStatus.Success; } async create(auth: AuthDto, dto: SessionCreateDto): Promise { @@ -51,7 +51,7 @@ export class SessionService extends BaseService { } async update(auth: AuthDto, id: string, dto: SessionUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.SESSION_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.SessionUpdate, ids: [id] }); if (Object.values(dto).filter((prop) => prop !== undefined).length === 0) { throw new BadRequestException('No fields to update'); @@ -65,12 +65,12 @@ export class SessionService extends BaseService { } async delete(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.AuthDeviceDelete, ids: [id] }); await this.sessionRepository.delete(id); } async lock(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.SessionLock, ids: [id] }); await this.sessionRepository.update(id, { pinExpiresAt: null }); } diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index b3b4c4b1cf..8e09580d55 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -95,26 +95,26 @@ describe(SharedLinkService.name, () => { describe('create', () => { it('should not allow an album shared link without an albumId', async () => { - await expect(sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [] })).rejects.toBeInstanceOf( + await expect(sut.create(authStub.admin, { type: SharedLinkType.Album, assetIds: [] })).rejects.toBeInstanceOf( BadRequestException, ); }); it('should not allow non-owners to create album shared links', async () => { await expect( - sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }), + sut.create(authStub.admin, { type: SharedLinkType.Album, assetIds: [], albumId: 'album-1' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should not allow individual shared links with no assets', async () => { await expect( - sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: [] }), + sut.create(authStub.admin, { type: SharedLinkType.Individual, assetIds: [] }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should require asset ownership to make an individual shared link', async () => { await expect( - sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: ['asset-1'] }), + sut.create(authStub.admin, { type: SharedLinkType.Individual, assetIds: ['asset-1'] }), ).rejects.toBeInstanceOf(BadRequestException); }); @@ -122,14 +122,14 @@ describe(SharedLinkService.name, () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid); - await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); + await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: albumStub.oneAsset.id }); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set([albumStub.oneAsset.id]), ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, userId: authStub.admin.user.id, albumId: albumStub.oneAsset.id, allowDownload: true, @@ -146,7 +146,7 @@ describe(SharedLinkService.name, () => { mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { - type: SharedLinkType.INDIVIDUAL, + type: SharedLinkType.Individual, assetIds: [assetStub.image.id], showMetadata: true, allowDownload: true, @@ -159,7 +159,7 @@ describe(SharedLinkService.name, () => { false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ - type: SharedLinkType.INDIVIDUAL, + type: SharedLinkType.Individual, userId: authStub.admin.user.id, albumId: null, allowDownload: true, @@ -177,7 +177,7 @@ describe(SharedLinkService.name, () => { mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { - type: SharedLinkType.INDIVIDUAL, + type: SharedLinkType.Individual, assetIds: [assetStub.image.id], showMetadata: false, allowDownload: true, @@ -190,7 +190,7 @@ describe(SharedLinkService.name, () => { false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ - type: SharedLinkType.INDIVIDUAL, + type: SharedLinkType.Individual, userId: authStub.admin.user.id, albumId: null, allowDownload: false, diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index c70b31a3a1..9f8e238c43 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -45,20 +45,20 @@ export class SharedLinkService extends BaseService { async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise { switch (dto.type) { - case SharedLinkType.ALBUM: { + case SharedLinkType.Album: { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); + await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [dto.albumId] }); break; } - case SharedLinkType.INDIVIDUAL: { + case SharedLinkType.Individual: { if (!dto.assetIds || dto.assetIds.length === 0) { throw new BadRequestException('Invalid assetIds'); } - await this.requireAccess({ auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); + await this.requireAccess({ auth, permission: Permission.AssetShare, ids: dto.assetIds }); break; } @@ -113,7 +113,7 @@ export class SharedLinkService extends BaseService { async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { + if (sharedLink.type !== SharedLinkType.Individual) { throw new BadRequestException('Invalid shared link type'); } @@ -121,7 +121,7 @@ export class SharedLinkService extends BaseService { const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); const allowedAssetIds = await this.checkAccess({ auth, - permission: Permission.ASSET_SHARE, + permission: Permission.AssetShare, ids: notPresentAssetIds, }); @@ -153,7 +153,7 @@ export class SharedLinkService extends BaseService { async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { + if (sharedLink.type !== SharedLinkType.Individual) { throw new BadRequestException('Invalid shared link type'); } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index a6529fa623..edd9f4663a 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -14,7 +14,7 @@ describe(SmartInfoService.name, () => { ({ sut, mocks } = newTestService(SmartInfoService)); mocks.asset.getByIds.mockResolvedValue([assetStub.image]); - mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); it('should work', () => { @@ -160,7 +160,7 @@ describe(SmartInfoService.name, () => { await sut.handleQueueEncodeClip({ force: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, + { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, ]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(false); expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); @@ -172,7 +172,7 @@ describe(SmartInfoService.name, () => { await sut.handleQueueEncodeClip({ force: true }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, + { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, ]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(true); expect(mocks.database.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); @@ -183,7 +183,7 @@ describe(SmartInfoService.name, () => { it('should do nothing if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED); + expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.Skipped); expect(mocks.asset.getByIds).not.toHaveBeenCalled(); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); @@ -192,7 +192,7 @@ describe(SmartInfoService.name, () => { it('should skip assets without a resize path', async () => { mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); - expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED); + expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed); expect(mocks.search.upsert).not.toHaveBeenCalled(); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); @@ -202,7 +202,7 @@ describe(SmartInfoService.name, () => { mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); + expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( ['http://immich-machine-learning:3003'], @@ -218,7 +218,7 @@ describe(SmartInfoService.name, () => { files: [assetStub.image.files[1]], }); - expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); @@ -227,7 +227,7 @@ describe(SmartInfoService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForClipEncoding.mockResolvedValue(void 0); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.FAILED); + expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Failed); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); @@ -238,7 +238,7 @@ describe(SmartInfoService.name, () => { mocks.database.isBusy.mockReturnValue(true); mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); + expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); expect(mocks.database.wait).toHaveBeenCalledWith(512); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index d6e30c6d86..3b8e2d1fc3 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -10,12 +10,12 @@ import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() export class SmartInfoService extends BaseService { - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] }) + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) async onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) { await this.init(newConfig); } - @OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true }) + @OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.Microservices], server: true }) async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) { await this.init(newConfig, oldConfig); } @@ -64,11 +64,11 @@ export class SmartInfoService extends BaseService { }); } - @OnJob({ name: JobName.QUEUE_SMART_SEARCH, queue: QueueName.SMART_SEARCH }) - async handleQueueEncodeClip({ force }: JobOf): Promise { + @OnJob({ name: JobName.SmartSearchQueueAll, queue: QueueName.SmartSearch }) + async handleQueueEncodeClip({ force }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } if (force) { @@ -80,7 +80,7 @@ export class SmartInfoService extends BaseService { let queue: JobItem[] = []; const assets = this.assetJobRepository.streamForEncodeClip(force); for await (const asset of assets) { - queue.push({ name: JobName.SMART_SEARCH, data: { id: asset.id } }); + queue.push({ name: JobName.SmartSearch, data: { id: asset.id } }); if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { await this.jobRepository.queueAll(queue); queue = []; @@ -89,23 +89,23 @@ export class SmartInfoService extends BaseService { await this.jobRepository.queueAll(queue); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.SMART_SEARCH, queue: QueueName.SMART_SEARCH }) - async handleEncodeClip({ id }: JobOf): Promise { + @OnJob({ name: JobName.SmartSearch, queue: QueueName.SmartSearch }) + async handleEncodeClip({ id }: JobOf): Promise { const { machineLearning } = await this.getConfig({ withCache: true }); if (!isSmartSearchEnabled(machineLearning)) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const asset = await this.assetJobRepository.getForClipEncoding(id); if (!asset || asset.files.length !== 1) { - return JobStatus.FAILED; + return JobStatus.Failed; } - if (asset.visibility === AssetVisibility.HIDDEN) { - return JobStatus.SKIPPED; + if (asset.visibility === AssetVisibility.Hidden) { + return JobStatus.Skipped; } const embedding = await this.machineLearningRepository.encodeImage( @@ -122,11 +122,11 @@ export class SmartInfoService extends BaseService { const newConfig = await this.getConfig({ withCache: true }); if (machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { // Skip the job if the the model has changed since the embedding was generated. - return JobStatus.SKIPPED; + return JobStatus.Skipped; } await this.searchRepository.upsert(asset.id, embedding); - return JobStatus.SUCCESS; + return JobStatus.Success; } } diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index b2ac47274f..18600abd12 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -17,7 +17,7 @@ export class StackService extends BaseService { } async create(auth: AuthDto, dto: StackCreateDto): Promise { - await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); const stack = await this.stackRepository.create({ ownerId: auth.user.id }, dto.assetIds); @@ -27,13 +27,13 @@ export class StackService extends BaseService { } async get(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.STACK_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.StackRead, ids: [id] }); const stack = await this.findOrFail(id); return mapStack(stack, { auth }); } async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.STACK_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] }); const stack = await this.findOrFail(id); if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { throw new BadRequestException('Primary asset must be in the stack'); @@ -47,13 +47,13 @@ export class StackService extends BaseService { } async delete(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.StackDelete, ids: [id] }); await this.stackRepository.delete(id); await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id }); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids }); + await this.requireAccess({ auth, permission: Permission.StackDelete, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id }); } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 9c4fe02f3e..882ffcd328 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,5 +1,6 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { AssetPathType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { albumStub } from 'test/fixtures/album.stub'; @@ -96,7 +97,7 @@ describe(StorageTemplateService.name, () => { it('should skip when storage template is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ storageTemplate: { enabled: false } }); - await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Skipped); expect(mocks.asset.getByIds).not.toHaveBeenCalled(); expect(mocks.storage.checkFileExists).not.toHaveBeenCalled(); @@ -110,8 +111,10 @@ describe(StorageTemplateService.name, () => { it('should migrate single moving picture', async () => { mocks.user.get.mockResolvedValue(userStub.user1); - const newMotionPicturePath = `upload/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; - const newStillPicturePath = `upload/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; + const newMotionPicturePath = + APP_MEDIA_LOCATION + `/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const newStillPicturePath = + APP_MEDIA_LOCATION + `/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset); mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); @@ -119,7 +122,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValueOnce({ id: '123', entityId: stillAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: stillAsset.originalPath, newPath: newStillPicturePath, }); @@ -127,12 +130,12 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValueOnce({ id: '124', entityId: motionAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: motionAsset.originalPath, newPath: newMotionPicturePath, }); - await expect(sut.handleMigrationSingle({ id: stillAsset.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleMigrationSingle({ id: stillAsset.id })).resolves.toBe(JobStatus.Success); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2); expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath }); @@ -152,13 +155,15 @@ describe(StorageTemplateService.name, () => { mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); mocks.album.getByAssetId.mockResolvedValueOnce([album]); - expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, - newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + newPath: expect.stringContaining( + `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + ), oldPath: asset.originalPath, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, }); }); @@ -172,14 +177,16 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(user); mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); - expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, - newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + newPath: expect.stringContaining( + `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + ), oldPath: asset.originalPath, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, }); }); @@ -206,14 +213,16 @@ describe(StorageTemplateService.name, () => { }, ]); - expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, - newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`, + newPath: expect.stringContaining( + `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`, + ), oldPath: asset.originalPath, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, }); }); @@ -229,14 +238,16 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(user); mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); - expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: asset.id, - newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`, + newPath: + APP_MEDIA_LOCATION + + `/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`, oldPath: asset.originalPath, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, }); }); @@ -244,14 +255,15 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(userStub.user1); const asset = assetStub.storageAsset(); - const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; - const newPath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; + const previousFailedNewPath = + APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; + const newPath = APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath)); mocks.move.getByEntity.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath: previousFailedNewPath, }); @@ -259,12 +271,12 @@ describe(StorageTemplateService.name, () => { mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(asset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3); @@ -284,8 +296,9 @@ describe(StorageTemplateService.name, () => { mocks.user.get.mockResolvedValue(userStub.user1); const asset = assetStub.storageAsset({ fileSizeInByte: 5000 }); - const previousFailedNewPath = `upload/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; - const newPath = `upload/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + const previousFailedNewPath = + APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; + const newPath = APP_MEDIA_LOCATION + `/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === previousFailedNewPath)); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); @@ -293,7 +306,7 @@ describe(StorageTemplateService.name, () => { mocks.move.getByEntity.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath: previousFailedNewPath, }); @@ -301,12 +314,12 @@ describe(StorageTemplateService.name, () => { mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: previousFailedNewPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(asset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3); @@ -319,7 +332,8 @@ describe(StorageTemplateService.name, () => { it('should fail move if copying and hash of asset and the new file do not match', async () => { mocks.user.get.mockResolvedValue(userStub.user1); - const newPath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; + const newPath = + APP_MEDIA_LOCATION + `/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); @@ -328,19 +342,19 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: testAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: testAsset.originalPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(testAsset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(1); expect(mocks.storage.stat).toHaveBeenCalledWith(newPath); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: testAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: testAsset.originalPath, newPath, }); @@ -370,7 +384,7 @@ describe(StorageTemplateService.name, () => { mocks.move.getByEntity.mockResolvedValue({ id: '123', entityId: testAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: testAsset.originalPath, newPath: previousFailedNewPath, }); @@ -378,12 +392,12 @@ describe(StorageTemplateService.name, () => { mocks.move.update.mockResolvedValue({ id: '123', entityId: testAsset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: previousFailedNewPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(testAsset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3); @@ -409,7 +423,7 @@ describe(StorageTemplateService.name, () => { it('should handle an asset with a duplicate destination', async () => { const asset = assetStub.storageAsset(); const oldPath = asset.originalPath; - const newPath = `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; const newPath2 = newPath.replace('.jpg', '+1.jpg'); mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); @@ -417,7 +431,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath, newPath, }); @@ -466,13 +480,13 @@ describe(StorageTemplateService.name, () => { it('should move an asset', async () => { const asset = assetStub.storageAsset(); const oldPath = asset.originalPath; - const newPath = `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', entityId: assetStub.image.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: assetStub.image.originalPath, newPath, }); @@ -492,7 +506,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath: `upload/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`, }); @@ -502,25 +516,27 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - `upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`, + expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, - originalPath: `upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`, + originalPath: expect.stringContaining( + `upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`, + ), }); }); it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => { const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 }); const oldPath = asset.originalPath; - const newPath = `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = APP_MEDIA_LOCATION + `/library/user-id/2022/2022-06-19/${asset.originalFileName}`; mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath, newPath, }); @@ -559,7 +575,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath: `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, }); @@ -572,14 +588,14 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, + expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.copyFile).toHaveBeenCalledWith( '/original/path.jpg', - `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, + expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.stat).toHaveBeenCalledWith( - `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, + expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -592,7 +608,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: 'move-123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: asset.originalPath, newPath: '', }); @@ -603,7 +619,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( '/original/path.jpg', - `upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`, + expect.stringContaining(`upload/library/user-id/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -622,7 +638,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, }); @@ -631,8 +647,8 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, - `upload/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`, + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), + expect.stringContaining(`upload/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`), ); }); @@ -648,7 +664,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`, }); @@ -657,8 +673,8 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`), + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`), ); }); @@ -674,7 +690,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, }); @@ -683,8 +699,8 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`, + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`), + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), ); }); @@ -700,7 +716,7 @@ describe(StorageTemplateService.name, () => { mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`, newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`, }); @@ -709,8 +725,8 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`, - `upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`, + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`), + expect.stringContaining(`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`), ); }); }); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index a286d518d6..6086d62809 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -97,7 +97,7 @@ export class StorageTemplateService extends BaseService { asset: { fileCreatedAt: new Date(), originalPath: '/upload/test/IMG_123.jpg', - type: AssetType.IMAGE, + type: AssetType.Image, id: 'd587e44b-f8c0-4832-9ba3-43268bbf5d4e', } as StorageAsset, filename: 'IMG_123', @@ -118,20 +118,20 @@ export class StorageTemplateService extends BaseService { @OnEvent({ name: 'AssetMetadataExtracted' }) async onAssetMetadataExtracted({ source, assetId }: ArgOf<'AssetMetadataExtracted'>) { - await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { source, id: assetId } }); + await this.jobRepository.queue({ name: JobName.StorageTemplateMigrationSingle, data: { source, id: assetId } }); } - @OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, queue: QueueName.STORAGE_TEMPLATE_MIGRATION }) - async handleMigrationSingle({ id }: JobOf): Promise { + @OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration }) + async handleMigrationSingle({ id }: JobOf): Promise { const config = await this.getConfig({ withCache: true }); const storageTemplateEnabled = config.storageTemplate.enabled; if (!storageTemplateEnabled) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } const asset = await this.assetJobRepository.getForStorageTemplateJob(id); if (!asset) { - return JobStatus.FAILED; + return JobStatus.Failed; } const user = await this.userRepository.get(asset.ownerId, {}); @@ -143,22 +143,22 @@ export class StorageTemplateService extends BaseService { if (asset.livePhotoVideoId) { const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId); if (!livePhotoVideo) { - return JobStatus.FAILED; + return JobStatus.Failed; } const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath); await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }); } - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION, queue: QueueName.STORAGE_TEMPLATE_MIGRATION }) + @OnJob({ name: JobName.StorageTemplateMigration, queue: QueueName.StorageTemplateMigration }) async handleMigration(): Promise { this.logger.log('Starting storage template migration'); const { storageTemplate } = await this.getConfig({ withCache: true }); const { enabled } = storageTemplate; if (!enabled) { this.logger.log('Storage template migration disabled, skipping'); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } await this.moveRepository.cleanMoveHistory(); @@ -174,12 +174,12 @@ export class StorageTemplateService extends BaseService { } this.logger.debug('Cleaning up empty directories...'); - const libraryFolder = StorageCore.getBaseFolder(StorageFolder.LIBRARY); + const libraryFolder = StorageCore.getBaseFolder(StorageFolder.Library); await this.storageRepository.removeEmptyDirs(libraryFolder); this.logger.log('Finished storage template migration'); - return JobStatus.SUCCESS; + return JobStatus.Success; } @OnEvent({ name: 'AssetDelete' }) @@ -208,7 +208,7 @@ export class StorageTemplateService extends BaseService { try { await this.storageCore.moveFile({ entityId: id, - pathType: AssetPathType.ORIGINAL, + pathType: AssetPathType.Original, oldPath, newPath, assetInfo: { sizeInBytes: fileSizeInByte, checksum }, @@ -216,7 +216,7 @@ export class StorageTemplateService extends BaseService { if (sidecarPath) { await this.storageCore.moveFile({ entityId: id, - pathType: AssetPathType.SIDECAR, + pathType: AssetPathType.Sidecar, oldPath: sidecarPath, newPath: `${newPath}.xmp`, }); @@ -357,8 +357,8 @@ export class StorageTemplateService extends BaseService { const substitutions: Record = { filename, ext: extension, - filetype: asset.type == AssetType.IMAGE ? 'IMG' : 'VID', - filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', + filetype: asset.type == AssetType.Image ? 'IMG' : 'VID', + filetypefull: asset.type == AssetType.Image ? 'IMAGE' : 'VIDEO', assetId: asset.id, assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 2d28489fae..567b78ac09 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -22,7 +22,7 @@ describe(StorageService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, { + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, { mountChecks: { backups: true, 'encoded-video': true, @@ -32,18 +32,36 @@ describe(StorageService.name, () => { upload: true, }, }); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/encoded-video'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/profile'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/upload'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups'); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer)); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/encoded-video')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/profile')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/thumbs')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/upload')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups')); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/encoded-video/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/library/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/profile/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/thumbs/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/upload/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/backups/.immich'), + expect.any(Buffer), + ); }); it('should enable mount folder checking for a new folder type', async () => { @@ -60,7 +78,7 @@ describe(StorageService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, { + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, { mountChecks: { backups: true, 'encoded-video': true, @@ -71,11 +89,17 @@ describe(StorageService.name, () => { }, }); expect(mocks.storage.mkdirSync).toHaveBeenCalledTimes(2); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library'); - expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups'); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/library')); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('upload/backups')); expect(mocks.storage.createFile).toHaveBeenCalledTimes(2); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); - expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer)); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/library/.immich'), + expect.any(Buffer), + ); + expect(mocks.storage.createFile).toHaveBeenCalledWith( + expect.stringContaining('upload/backups/.immich'), + expect.any(Buffer), + ); }); it('should throw an error if .immich is missing', async () => { @@ -131,7 +155,7 @@ describe(StorageService.name, () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); + expect(mocks.systemMetadata.set).not.toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, expect.anything()); }); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index e9ca10f08a..632e0c1385 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { join, resolve } from 'node:path'; +import { join } from 'node:path'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { DatabaseLock, JobName, JobStatus, QueueName, StorageFolder, SystemMetadataKey } from 'src/enum'; @@ -17,7 +18,7 @@ export class StorageService extends BaseService { await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { const flags = - (await this.systemMetadataRepository.get(SystemMetadataKey.SYSTEM_FLAGS)) || + (await this.systemMetadataRepository.get(SystemMetadataKey.SystemFlags)) || ({ mountChecks: {} } as SystemFlags); if (!flags.mountChecks) { @@ -46,7 +47,7 @@ export class StorageService extends BaseService { } if (updated) { - await this.systemMetadataRepository.set(SystemMetadataKey.SYSTEM_FLAGS, flags); + await this.systemMetadataRepository.set(SystemMetadataKey.SystemFlags, flags); this.logger.log('Successfully enabled system mount folders checks'); } @@ -60,10 +61,21 @@ export class StorageService extends BaseService { } } }); + + await this.databaseRepository.withLock(DatabaseLock.MediaLocation, async () => { + const current = APP_MEDIA_LOCATION; + const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation); + const previous = savedValue?.location || ''; + + if (previous !== current) { + this.logger.log(`Media location changed (from=${previous}, to=${current})`); + await this.systemMetadataRepository.set(SystemMetadataKey.MediaLocation, { location: current }); + } + }); } - @OnJob({ name: JobName.DELETE_FILES, queue: QueueName.BACKGROUND_TASK }) - async handleDeleteFiles(job: JobOf): Promise { + @OnJob({ name: JobName.FileDelete, queue: QueueName.BackgroundTask }) + async handleDeleteFiles(job: JobOf): Promise { const { files } = job; // TODO: one job per file @@ -79,7 +91,7 @@ export class StorageService extends BaseService { } } - return JobStatus.SUCCESS; + return JobStatus.Success; } private async verifyReadAccess(folder: StorageFolder) { @@ -87,9 +99,8 @@ export class StorageService extends BaseService { try { await this.storageRepository.readFile(internalPath); } catch (error) { - const fullyQualifiedPath = resolve(process.cwd(), internalPath); - this.logger.error(`Failed to read ${fullyQualifiedPath} (${internalPath}): ${error}`); - throw new ImmichStartupError(`Failed to read: "${externalPath} (${fullyQualifiedPath}) - ${docsMessage}"`); + this.logger.error(`Failed to read (${internalPath}): ${error}`); + throw new ImmichStartupError(`Failed to read: "${externalPath} (${internalPath}) - ${docsMessage}"`); } } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 9779498d70..fb582ab038 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -70,6 +70,7 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.MemoriesV1, SyncRequestType.MemoryToAssetsV1, SyncRequestType.PeopleV1, + SyncRequestType.AssetFacesV1, SyncRequestType.UserMetadataV1, ]; @@ -156,6 +157,7 @@ export class SyncService extends BaseService { [SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth), [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, session.id), [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth), + [SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(response, checkpointMap, auth), [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(response, checkpointMap, auth), }; @@ -606,6 +608,20 @@ export class SyncService extends BaseService { } } + private async syncAssetFacesV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) { + const deleteType = SyncEntityType.AssetFaceDeleteV1; + const deletes = this.syncRepository.assetFace.getDeletes(auth.user.id, checkpointMap[deleteType]); + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.AssetFaceV1; + const upserts = this.syncRepository.assetFace.getUpserts(auth.user.id, checkpointMap[upsertType]); + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async syncUserMetadataV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) { const deleteType = SyncEntityType.UserMetadataDeleteV1; const deletes = this.syncRepository.userMetadata.getDeletes(auth.user.id, checkpointMap[deleteType]); @@ -640,7 +656,7 @@ export class SyncService extends BaseService { async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; - await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] }); + await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [userId] }); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, updatedUntil: dto.updatedUntil, @@ -664,7 +680,7 @@ export class SyncService extends BaseService { return FULL_SYNC; } - await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); + await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: dto.userIds }); const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); @@ -676,8 +692,8 @@ export class SyncService extends BaseService { const deleted = await this.auditRepository.getAfter(dto.updatedAfter, { userIds, - entityType: EntityType.ASSET, - action: DatabaseAction.DELETE, + entityType: EntityType.Asset, + action: DatabaseAction.Delete, }); const result = { @@ -686,7 +702,7 @@ export class SyncService extends BaseService { // do not return archived assets for partner users .filter( (a) => - a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.TIMELINE), + a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.Timeline), ) .map((a) => mapAsset(a, { diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 582c50ed8a..20127bab15 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -9,7 +9,7 @@ import { OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, - TranscodeHWAccel, + TranscodeHardwareAcceleration, TranscodePolicy, VideoCodec, VideoContainer, @@ -28,17 +28,17 @@ const partialConfig = { const updatedConfig = Object.freeze({ job: { - [QueueName.BACKGROUND_TASK]: { concurrency: 5 }, - [QueueName.SMART_SEARCH]: { concurrency: 2 }, - [QueueName.METADATA_EXTRACTION]: { concurrency: 5 }, - [QueueName.FACE_DETECTION]: { concurrency: 2 }, - [QueueName.SEARCH]: { concurrency: 5 }, - [QueueName.SIDECAR]: { concurrency: 5 }, - [QueueName.LIBRARY]: { concurrency: 5 }, - [QueueName.MIGRATION]: { concurrency: 5 }, - [QueueName.THUMBNAIL_GENERATION]: { concurrency: 3 }, - [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, - [QueueName.NOTIFICATION]: { concurrency: 5 }, + [QueueName.BackgroundTask]: { concurrency: 5 }, + [QueueName.SmartSearch]: { concurrency: 2 }, + [QueueName.MetadataExtraction]: { concurrency: 5 }, + [QueueName.FaceDetection]: { concurrency: 2 }, + [QueueName.Search]: { concurrency: 5 }, + [QueueName.Sidecar]: { concurrency: 5 }, + [QueueName.Library]: { concurrency: 5 }, + [QueueName.Migration]: { concurrency: 5 }, + [QueueName.ThumbnailGeneration]: { concurrency: 3 }, + [QueueName.VideoConversion]: { concurrency: 1 }, + [QueueName.Notification]: { concurrency: 5 }, }, backup: { database: { @@ -51,28 +51,28 @@ const updatedConfig = Object.freeze({ crf: 30, threads: 0, preset: 'ultrafast', - targetAudioCodec: AudioCodec.AAC, - acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE], + targetAudioCodec: AudioCodec.Aac, + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le], targetResolution: '720', targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], - acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM], + acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], maxBitrate: '0', bframes: -1, refs: 0, gopSize: 0, temporalAQ: false, - cqMode: CQMode.AUTO, + cqMode: CQMode.Auto, twoPass: false, preferredHwDevice: 'auto', - transcode: TranscodePolicy.REQUIRED, - accel: TranscodeHWAccel.DISABLED, + transcode: TranscodePolicy.Required, + accel: TranscodeHardwareAcceleration.Disabled, accelDecode: false, - tonemap: ToneMapping.HABLE, + tonemap: ToneMapping.Hable, }, logging: { enabled: true, - level: LogLevel.LOG, + level: LogLevel.Log, }, metadata: { faces: { @@ -128,7 +128,7 @@ const updatedConfig = Object.freeze({ scope: 'openid email profile', signingAlgorithm: 'RS256', profileSigningAlgorithm: 'none', - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, timeout: 30_000, storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', @@ -150,15 +150,15 @@ const updatedConfig = Object.freeze({ image: { thumbnail: { size: 250, - format: ImageFormat.WEBP, + format: ImageFormat.Webp, quality: 80, }, preview: { size: 1440, - format: ImageFormat.JPEG, + format: ImageFormat.Jpeg, quality: 80, }, - fullsize: { enabled: false, format: ImageFormat.JPEG, quality: 80 }, + fullsize: { enabled: false, format: ImageFormat.Jpeg, quality: 80 }, colorspace: Colorspace.P3, extractEmbedded: false, }, diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts index a8d6c0cdcc..f5bdcde7b4 100644 --- a/server/src/services/system-metadata.service.spec.ts +++ b/server/src/services/system-metadata.service.spec.ts @@ -30,12 +30,12 @@ describe(SystemMetadataService.name, () => { describe('updateAdminOnboarding', () => { it('should update isOnboarded to true', async () => { await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.AdminOnboarding, { isOnboarded: true }); }); it('should update isOnboarded to false', async () => { await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.AdminOnboarding, { isOnboarded: false }); }); }); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts index 750e6b1d0b..30af715379 100644 --- a/server/src/services/system-metadata.service.ts +++ b/server/src/services/system-metadata.service.ts @@ -11,23 +11,23 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class SystemMetadataService extends BaseService { async getAdminOnboarding(): Promise { - const value = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING); + const value = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { isOnboarded: false, ...value }; } async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { - await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + await this.systemMetadataRepository.set(SystemMetadataKey.AdminOnboarding, { isOnboarded: dto.isOnboarded, }); } async getReverseGeocodingState(): Promise { - const value = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + const value = await this.systemMetadataRepository.get(SystemMetadataKey.ReverseGeocodingState); return { lastUpdate: null, lastImportFileName: null, ...value }; } async getVersionCheckState(): Promise { - const value = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); + const value = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); return { checkedAt: null, releaseVersion: null, ...value }; } } diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 70507ab433..6699c61970 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -278,7 +278,7 @@ describe(TagService.name, () => { it('should delete empty tags', async () => { mocks.tag.deleteEmptyTags.mockResolvedValue(); - await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.Success); expect(mocks.tag.deleteEmptyTags).toHaveBeenCalled(); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index e975fc3980..2fae4b55d0 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -26,7 +26,7 @@ export class TagService extends BaseService { } async get(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.TagRead, ids: [id] }); const tag = await this.findOrFail(id); return mapTag(tag); } @@ -34,7 +34,7 @@ export class TagService extends BaseService { async create(auth: AuthDto, dto: TagCreateDto) { let parent; if (dto.parentId) { - await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); + await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.parentId] }); parent = await this.tagRepository.get(dto.parentId); if (!parent) { throw new BadRequestException('Tag not found'); @@ -55,7 +55,7 @@ export class TagService extends BaseService { } async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.TAG_UPDATE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.TagUpdate, ids: [id] }); const { color } = dto; const tag = await this.tagRepository.update(id, { color }); @@ -68,7 +68,7 @@ export class TagService extends BaseService { } async remove(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.TAG_DELETE, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.TagDelete, ids: [id] }); // TODO sync tag changes for affected assets @@ -77,8 +77,8 @@ export class TagService extends BaseService { async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise { const [tagIds, assetIds] = await Promise.all([ - this.checkAccess({ auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), - this.checkAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), + this.checkAccess({ auth, permission: Permission.TagAsset, ids: dto.tagIds }), + this.checkAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }), ]); const items: Insertable[] = []; @@ -97,7 +97,7 @@ export class TagService extends BaseService { } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.TagAsset, ids: [id] }); const results = await addAssets( auth, @@ -115,12 +115,12 @@ export class TagService extends BaseService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] }); + await this.requireAccess({ auth, permission: Permission.TagAsset, ids: [id] }); const results = await removeAssets( auth, { access: this.accessRepository, bulk: this.tagRepository }, - { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE }, + { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TagDelete }, ); for (const { id: assetId, success } of results) { @@ -132,10 +132,10 @@ export class TagService extends BaseService { return results; } - @OnJob({ name: JobName.TAG_CLEANUP, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.TagCleanup, queue: QueueName.BackgroundTask }) async handleTagCleanup() { await this.tagRepository.deleteEmptyTags(); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async findOrFail(id: string) { diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 1669b1eac7..11df30a7d4 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -49,7 +49,7 @@ describe(TimelineService.name, () => { await expect( sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', - visibility: AssetVisibility.ARCHIVE, + visibility: AssetVisibility.Archive, userId: authStub.admin.user.id, }), ).resolves.toEqual(json); @@ -57,7 +57,7 @@ describe(TimelineService.name, () => { 'bucket', expect.objectContaining({ timeBucket: 'bucket', - visibility: AssetVisibility.ARCHIVE, + visibility: AssetVisibility.Archive, userIds: [authStub.admin.user.id], }), ); @@ -71,14 +71,14 @@ describe(TimelineService.name, () => { await expect( sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, userId: authStub.admin.user.id, withPartners: true, }), ).resolves.toEqual(json); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { timeBucket: 'bucket', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, withPartners: true, userIds: [authStub.admin.user.id], }); @@ -126,7 +126,7 @@ describe(TimelineService.name, () => { await expect( sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', - visibility: AssetVisibility.ARCHIVE, + visibility: AssetVisibility.Archive, withPartners: true, userId: authStub.admin.user.id, }), diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index abd536a97e..d8cac3a205 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -45,29 +45,29 @@ export class TimelineService extends BaseService { } private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { - if (dto.visibility === AssetVisibility.LOCKED) { + if (dto.visibility === AssetVisibility.Locked) { requireElevatedPermission(auth); } if (dto.albumId) { - await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] }); } else { dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); - if (dto.visibility === AssetVisibility.ARCHIVE) { - await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); + await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [dto.userId] }); + if (dto.visibility === AssetVisibility.Archive) { + await this.requireAccess({ auth, permission: Permission.ArchiveRead, ids: [dto.userId] }); } } if (dto.tagId) { - await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.tagId] }); + await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.tagId] }); } if (dto.withPartners) { - const requestedArchived = dto.visibility === AssetVisibility.ARCHIVE || dto.visibility === undefined; + const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined; const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; const requestedTrash = dto.isTrashed === true; diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index b3bee90815..e43c49e543 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -77,24 +77,24 @@ describe(TrashService.name, () => { mocks.trash.empty.mockResolvedValue(1); await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 }); expect(mocks.trash.empty).toHaveBeenCalledWith('user-id'); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEmptyTrash, data: {} }); }); }); describe('onAssetsDelete', () => { it('should queue the empty trash job', async () => { await expect(sut.onAssetsDelete()).resolves.toBeUndefined(); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEmptyTrash, data: {} }); }); }); describe('handleQueueEmptyTrash', () => { it('should queue asset delete jobs', async () => { mocks.trash.getDeletedIds.mockReturnValue(makeAssetIdStream(1)); - await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleEmptyTrash()).resolves.toEqual(JobStatus.Success); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: 'asset-1', deleteOnDisk: true }, }, ]); diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index a7447ab890..f1d368111e 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -15,7 +15,7 @@ export class TrashService extends BaseService { return { count: 0 }; } - await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids }); + await this.requireAccess({ auth, permission: Permission.AssetDelete, ids }); await this.trashRepository.restoreAll(ids); await this.eventRepository.emit('AssetRestoreAll', { assetIds: ids, userId: auth.user.id }); @@ -35,18 +35,18 @@ export class TrashService extends BaseService { async empty(auth: AuthDto): Promise { const count = await this.trashRepository.empty(auth.user.id); if (count > 0) { - await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + await this.jobRepository.queue({ name: JobName.AssetEmptyTrash, data: {} }); } return { count }; } @OnEvent({ name: 'AssetDeleteAll' }) async onAssetsDelete() { - await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + await this.jobRepository.queue({ name: JobName.AssetEmptyTrash, data: {} }); } - @OnJob({ name: JobName.QUEUE_TRASH_EMPTY, queue: QueueName.BACKGROUND_TASK }) - async handleQueueEmptyTrash() { + @OnJob({ name: JobName.AssetEmptyTrash, queue: QueueName.BackgroundTask }) + async handleEmptyTrash() { const assets = this.trashRepository.getDeletedIds(); let count = 0; @@ -67,14 +67,14 @@ export class TrashService extends BaseService { this.logger.log(`Queued ${count} asset(s) for deletion from the trash`); - return JobStatus.SUCCESS; + return JobStatus.Success; } private async handleBatch(ids: string[]) { this.logger.debug(`Queueing ${ids.length} asset(s) for deletion from the trash`); await this.jobRepository.queueAll( ids.map((assetId) => ({ - name: JobName.ASSET_DELETION, + name: JobName.AssetDelete, data: { id: assetId, deleteOnDisk: true, diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 85cbb8238a..d8e13fcfbd 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -140,7 +140,7 @@ describe(UserAdminService.name, () => { await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUserAdmin(userStub.user1)); expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, { - status: UserStatus.DELETED, + status: UserStatus.Deleted, deletedAt: expect.any(Date), }); }); @@ -154,11 +154,11 @@ describe(UserAdminService.name, () => { ); expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, { - status: UserStatus.REMOVING, + status: UserStatus.Removing, deletedAt: expect.any(Date), }); expect(mocks.job.queue).toHaveBeenCalledWith({ - name: JobName.USER_DELETION, + name: JobName.UserDelete, data: { id: userStub.user1.id, force: true }, }); }); diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 180471bb44..3ae9d429eb 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -100,11 +100,11 @@ export class UserAdminService extends BaseService { await this.albumRepository.softDeleteAll(id); - const status = force ? UserStatus.REMOVING : UserStatus.DELETED; + const status = force ? UserStatus.Removing : UserStatus.Deleted; const user = await this.userRepository.update(id, { status, deletedAt: new Date() }); if (force) { - await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } }); + await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } }); } return mapUserAdmin(user); @@ -134,7 +134,7 @@ export class UserAdminService extends BaseService { const newPreferences = mergePreferences(getPreferences(metadata), dto); await this.userRepository.upsertMetadata(id, { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: getPreferencesPartial(newPreferences), }); diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 4ee92fc3d3..b4e616974e 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -122,7 +122,7 @@ describe(UserService.name, () => { await sut.createProfileImage(authStub.admin, file); - expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.FileDelete, data: { files } }]]); }); it('should not delete the profile image if it has not been set', async () => { @@ -156,7 +156,7 @@ describe(UserService.name, () => { await sut.deleteProfileImage(authStub.admin); - expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.FileDelete, data: { files } }]]); }); }); @@ -185,7 +185,7 @@ describe(UserService.name, () => { new ImmichFileResponse({ path: '/path/to/profile.jpg', contentType: 'image/jpeg', - cacheControl: CacheControl.NONE, + cacheControl: CacheControl.None, }), ); @@ -211,7 +211,7 @@ describe(UserService.name, () => { await sut.handleUserDeleteCheck(); expect(mocks.user.getDeletedAfter).toHaveBeenCalled(); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.UserDelete, data: { id: user.id } }]); }); }); @@ -235,11 +235,26 @@ describe(UserService.name, () => { await sut.handleUserDelete({ id: user.id }); - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/library/deleted-user', options); - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/upload/deleted-user', options); - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/profile/deleted-user', options); - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options); - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( + expect.stringContaining('upload/library/deleted-user'), + options, + ); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( + expect.stringContaining('upload/upload/deleted-user'), + options, + ); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( + expect.stringContaining('upload/profile/deleted-user'), + options, + ); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( + expect.stringContaining('upload/thumbs/deleted-user'), + options, + ); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith( + expect.stringContaining('upload/encoded-video/deleted-user'), + options, + ); expect(mocks.album.deleteAll).toHaveBeenCalledWith(user.id); expect(mocks.user.delete).toHaveBeenCalledWith(user, true); }); @@ -253,7 +268,7 @@ describe(UserService.name, () => { const options = { force: true, recursive: true }; - expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options); + expect(mocks.storage.unlinkDir).toHaveBeenCalledWith(expect.stringContaining('upload/library/admin'), options); }); }); @@ -266,7 +281,7 @@ describe(UserService.name, () => { await sut.setLicense(authStub.user1, license); expect(mocks.user.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, { - key: UserMetadataKey.LICENSE, + key: UserMetadataKey.License, value: expect.any(Object), }); }); @@ -279,7 +294,7 @@ describe(UserService.name, () => { await sut.setLicense(authStub.user1, license); expect(mocks.user.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, { - key: UserMetadataKey.LICENSE, + key: UserMetadataKey.License, value: expect.any(Object), }); }); diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 78f49fd7ae..6849b17ac3 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -78,7 +78,7 @@ export class UserService extends BaseService { const updated = mergePreferences(getPreferences(metadata), dto); await this.userRepository.upsertMetadata(auth.user.id, { - key: UserMetadataKey.PREFERENCES, + key: UserMetadataKey.Preferences, value: getPreferencesPartial(updated), }); @@ -99,7 +99,7 @@ export class UserService extends BaseService { }); if (oldpath !== '') { - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldpath] } }); } return { @@ -115,7 +115,7 @@ export class UserService extends BaseService { throw new BadRequestException("Can't delete a missing profile Image"); } await this.userRepository.update(auth.user.id, { profileImagePath: '', profileChangedAt: new Date() }); - await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [user.profileImagePath] } }); } async getProfileImage(id: string): Promise { @@ -127,7 +127,7 @@ export class UserService extends BaseService { return new ImmichFileResponse({ path: user.profileImagePath, contentType: 'image/jpeg', - cacheControl: CacheControl.NONE, + cacheControl: CacheControl.None, }); } @@ -135,7 +135,7 @@ export class UserService extends BaseService { const metadata = await this.userRepository.getMetadata(auth.user.id); const license = metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.LICENSE, + (item): item is UserMetadataItem => item.key === UserMetadataKey.License, ); if (!license) { throw new NotFoundException(); @@ -144,7 +144,7 @@ export class UserService extends BaseService { } async deleteLicense({ user }: AuthDto): Promise { - await this.userRepository.deleteMetadata(user.id, UserMetadataKey.LICENSE); + await this.userRepository.deleteMetadata(user.id, UserMetadataKey.License); } async setLicense(auth: AuthDto, license: LicenseKeyDto): Promise { @@ -173,7 +173,7 @@ export class UserService extends BaseService { const activatedAt = new Date(); await this.userRepository.upsertMetadata(auth.user.id, { - key: UserMetadataKey.LICENSE, + key: UserMetadataKey.License, value: { ...license, activatedAt: activatedAt.toISOString() }, }); @@ -184,7 +184,7 @@ export class UserService extends BaseService { const metadata = await this.userRepository.getMetadata(auth.user.id); const onboardingData = metadata.find( - (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + (item): item is UserMetadataItem => item.key === UserMetadataKey.Onboarding, )?.value; if (!onboardingData) { @@ -197,12 +197,12 @@ export class UserService extends BaseService { } async deleteOnboarding({ user }: AuthDto): Promise { - await this.userRepository.deleteMetadata(user.id, UserMetadataKey.ONBOARDING); + await this.userRepository.deleteMetadata(user.id, UserMetadataKey.Onboarding); } async setOnboarding(auth: AuthDto, onboarding: OnboardingDto): Promise { await this.userRepository.upsertMetadata(auth.user.id, { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, value: { isOnboarded: onboarding.isOnboarded, }, @@ -213,42 +213,42 @@ export class UserService extends BaseService { }; } - @OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); - return JobStatus.SUCCESS; + return JobStatus.Success; } - @OnJob({ name: JobName.USER_DELETE_CHECK, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.UserDeleteCheck, queue: QueueName.BackgroundTask }) async handleUserDeleteCheck(): Promise { const config = await this.getConfig({ withCache: false }); const users = await this.userRepository.getDeletedAfter(DateTime.now().minus({ days: config.user.deleteDelay })); - await this.jobRepository.queueAll(users.map((user) => ({ name: JobName.USER_DELETION, data: { id: user.id } }))); - return JobStatus.SUCCESS; + await this.jobRepository.queueAll(users.map((user) => ({ name: JobName.UserDelete, data: { id: user.id } }))); + return JobStatus.Success; } - @OnJob({ name: JobName.USER_DELETION, queue: QueueName.BACKGROUND_TASK }) - async handleUserDelete({ id, force }: JobOf): Promise { + @OnJob({ name: JobName.UserDelete, queue: QueueName.BackgroundTask }) + async handleUserDelete({ id, force }: JobOf): Promise { const config = await this.getConfig({ withCache: false }); const user = await this.userRepository.get(id, { withDeleted: true }); if (!user) { - return JobStatus.FAILED; + return JobStatus.Failed; } // just for extra protection here if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) { this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`); - return JobStatus.SKIPPED; + return JobStatus.Skipped; } this.logger.log(`Deleting user: ${user.id}`); const folders = [ StorageCore.getLibraryFolder(user), - StorageCore.getFolderLocation(StorageFolder.UPLOAD, user.id), - StorageCore.getFolderLocation(StorageFolder.PROFILE, user.id), - StorageCore.getFolderLocation(StorageFolder.THUMBNAILS, user.id), - StorageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, user.id), + StorageCore.getFolderLocation(StorageFolder.Upload, user.id), + StorageCore.getFolderLocation(StorageFolder.Profile, user.id), + StorageCore.getFolderLocation(StorageFolder.Thumbnails, user.id), + StorageCore.getFolderLocation(StorageFolder.EncodedVideo, user.id), ]; for (const folder of folders) { @@ -260,7 +260,7 @@ export class UserService extends BaseService { await this.albumRepository.deleteAll(user.id); await this.userRepository.delete(user, true); - return JobStatus.SUCCESS; + return JobStatus.Success; } private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean { diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index a83d9f85b6..73794275ea 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -72,18 +72,18 @@ describe(VersionService.name, () => { describe('handQueueVersionCheck', () => { it('should queue a version check job', async () => { await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined(); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VERSION_CHECK, data: {} }); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} }); }); }); describe('handVersionCheck', () => { beforeEach(() => { - mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.PRODUCTION })); + mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.Production })); }); it('should not run in dev mode', async () => { - mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.DEVELOPMENT })); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED); + mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.Development })); + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped); }); it('should not run if the last check was < 60 minutes ago', async () => { @@ -91,12 +91,12 @@ describe(VersionService.name, () => { checkedAt: DateTime.utc().minus({ minutes: 5 }).toISO(), releaseVersion: '1.0.0', }); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED); + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped); }); it('should not run if version check is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ newVersionCheck: { enabled: false } }); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED); + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped); }); it('should run if it has been > 60 minutes', async () => { @@ -105,7 +105,7 @@ describe(VersionService.name, () => { checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(), releaseVersion: '1.0.0', }); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success); expect(mocks.systemMetadata.set).toHaveBeenCalled(); expect(mocks.logger.log).toHaveBeenCalled(); expect(mocks.event.clientBroadcast).toHaveBeenCalled(); @@ -113,8 +113,8 @@ describe(VersionService.name, () => { it('should not notify if the version is equal', async () => { mocks.serverInfo.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString())); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, { + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success); + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.VersionCheckState, { checkedAt: expect.any(String), releaseVersion: serverVersion.toString(), }); @@ -123,7 +123,7 @@ describe(VersionService.name, () => { it('should handle a github error', async () => { mocks.serverInfo.getGitHubRelease.mockRejectedValue(new Error('GitHub is down')); - await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.FAILED); + await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Failed); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); expect(mocks.event.clientBroadcast).not.toHaveBeenCalled(); expect(mocks.logger.warn).toHaveBeenCalled(); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 51d31b623f..c4d7e9974d 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -41,7 +41,7 @@ export class VersionService extends BaseService { const needsNewMemories = semver.lt(previousVersion, '1.129.0'); if (needsNewMemories) { - await this.jobRepository.queue({ name: JobName.MEMORIES_CREATE }); + await this.jobRepository.queue({ name: JobName.MemoryGenerate }); } } }); @@ -56,31 +56,31 @@ export class VersionService extends BaseService { } async handleQueueVersionCheck() { - await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} }); + await this.jobRepository.queue({ name: JobName.VersionCheck, data: {} }); } - @OnJob({ name: JobName.VERSION_CHECK, queue: QueueName.BACKGROUND_TASK }) + @OnJob({ name: JobName.VersionCheck, queue: QueueName.BackgroundTask }) async handleVersionCheck(): Promise { try { this.logger.debug('Running version check'); const { environment } = this.configRepository.getEnv(); - if (environment === ImmichEnvironment.DEVELOPMENT) { - return JobStatus.SKIPPED; + if (environment === ImmichEnvironment.Development) { + return JobStatus.Skipped; } const { newVersionCheck } = await this.getConfig({ withCache: true }); if (!newVersionCheck.enabled) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } - const versionCheck = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); + const versionCheck = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); if (versionCheck?.checkedAt) { const lastUpdate = DateTime.fromISO(versionCheck.checkedAt); const elapsedTime = DateTime.now().diff(lastUpdate).as('minutes'); // check once per hour (max) if (elapsedTime < 60) { - return JobStatus.SKIPPED; + return JobStatus.Skipped; } } @@ -88,7 +88,7 @@ export class VersionService extends BaseService { await this.serverInfoRepository.getGitHubRelease(); const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion }; - await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata); + await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata); if (semver.gt(releaseVersion, serverVersion)) { this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); @@ -96,16 +96,16 @@ export class VersionService extends BaseService { } } catch (error: Error | any) { this.logger.warn(`Unable to run version check: ${error}`, error?.stack); - return JobStatus.FAILED; + return JobStatus.Failed; } - return JobStatus.SUCCESS; + return JobStatus.Success; } @OnEvent({ name: 'WebsocketConnect' }) async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) { this.eventRepository.clientSend('on_server_version', userId, serverVersion); - const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); + const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); if (metadata) { this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata)); } diff --git a/server/src/types.ts b/server/src/types.ts index 6776604078..9cd1aa996b 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -161,10 +161,10 @@ export interface VideoInterfaces { export type ConcurrentQueueName = Exclude< QueueName, - | QueueName.STORAGE_TEMPLATE_MIGRATION - | QueueName.FACIAL_RECOGNITION - | QueueName.DUPLICATE_DETECTION - | QueueName.BACKUP_DATABASE + | QueueName.StorageTemplateMigration + | QueueName.FacialRecognition + | QueueName.DuplicateDetection + | QueueName.BackupDatabase >; export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] }; @@ -273,96 +273,96 @@ export interface QueueStatus { export type JobItem = // Backups - | { name: JobName.BACKUP_DATABASE; data?: IBaseJob } + | { name: JobName.DatabaseBackup; data?: IBaseJob } // Transcoding - | { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob } - | { name: JobName.VIDEO_CONVERSION; data: IEntityJob } + | { name: JobName.AssetEncodeVideoQueueAll; data: IBaseJob } + | { name: JobName.AssetEncodeVideo; data: IEntityJob } // Thumbnails - | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } - | { name: JobName.GENERATE_THUMBNAILS; data: IEntityJob } + | { name: JobName.AssetGenerateThumbnailsQueueAll; data: IBaseJob } + | { name: JobName.AssetGenerateThumbnails; data: IEntityJob } // User - | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } - | { name: JobName.USER_DELETION; data: IEntityJob } - | { name: JobName.USER_SYNC_USAGE; data?: IBaseJob } + | { name: JobName.UserDeleteCheck; data?: IBaseJob } + | { name: JobName.UserDelete; data: IEntityJob } + | { name: JobName.UserSyncUsage; data?: IBaseJob } // Storage Template - | { name: JobName.STORAGE_TEMPLATE_MIGRATION; data?: IBaseJob } - | { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE; data: IEntityJob } + | { name: JobName.StorageTemplateMigration; data?: IBaseJob } + | { name: JobName.StorageTemplateMigrationSingle; data: IEntityJob } // Migration - | { name: JobName.QUEUE_MIGRATION; data?: IBaseJob } - | { name: JobName.MIGRATE_ASSET; data: IEntityJob } - | { name: JobName.MIGRATE_PERSON; data: IEntityJob } + | { name: JobName.FileMigrationQueueAll; data?: IBaseJob } + | { name: JobName.AssetFileMigration; data: IEntityJob } + | { name: JobName.PersonFileMigration; data: IEntityJob } // Metadata Extraction - | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob } - | { name: JobName.METADATA_EXTRACTION; data: IEntityJob } + | { name: JobName.AssetExtractMetadataQueueAll; data: IBaseJob } + | { name: JobName.AssetExtractMetadata; data: IEntityJob } // Notifications - | { name: JobName.NOTIFICATIONS_CLEANUP; data?: IBaseJob } + | { name: JobName.NotificationsCleanup; data?: IBaseJob } // Sidecar Scanning - | { name: JobName.QUEUE_SIDECAR; data: IBaseJob } - | { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob } - | { name: JobName.SIDECAR_SYNC; data: IEntityJob } - | { name: JobName.SIDECAR_WRITE; data: ISidecarWriteJob } + | { name: JobName.SidecarQueueAll; data: IBaseJob } + | { name: JobName.SidecarDiscovery; data: IEntityJob } + | { name: JobName.SidecarSync; data: IEntityJob } + | { name: JobName.SidecarWrite; data: ISidecarWriteJob } // Facial Recognition - | { name: JobName.QUEUE_FACE_DETECTION; data: IBaseJob } - | { name: JobName.FACE_DETECTION; data: IEntityJob } - | { name: JobName.QUEUE_FACIAL_RECOGNITION; data: INightlyJob } - | { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob } - | { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob } + | { name: JobName.AssetDetectFacesQueueAll; data: IBaseJob } + | { name: JobName.AssetDetectFaces; data: IEntityJob } + | { name: JobName.FacialRecognitionQueueAll; data: INightlyJob } + | { name: JobName.FacialRecognition; data: IDeferrableJob } + | { name: JobName.PersonGenerateThumbnail; data: IEntityJob } // Smart Search - | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob } - | { name: JobName.SMART_SEARCH; data: IEntityJob } - | { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob } + | { name: JobName.SmartSearchQueueAll; data: IBaseJob } + | { name: JobName.SmartSearch; data: IEntityJob } + | { name: JobName.AssetEmptyTrash; data?: IBaseJob } // Duplicate Detection - | { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob } - | { name: JobName.DUPLICATE_DETECTION; data: IEntityJob } + | { name: JobName.AssetDetectDuplicatesQueueAll; data: IBaseJob } + | { name: JobName.AssetDetectDuplicates; data: IEntityJob } // Memories - | { name: JobName.MEMORIES_CLEANUP; data?: IBaseJob } - | { name: JobName.MEMORIES_CREATE; data?: IBaseJob } + | { name: JobName.MemoryCleanup; data?: IBaseJob } + | { name: JobName.MemoryGenerate; data?: IBaseJob } // Filesystem - | { name: JobName.DELETE_FILES; data: IDeleteFilesJob } + | { name: JobName.FileDelete; data: IDeleteFilesJob } // Cleanup - | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob } - | { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob } + | { name: JobName.AuditLogCleanup; data?: IBaseJob } + | { name: JobName.SessionCleanup; data?: IBaseJob } // Tags - | { name: JobName.TAG_CLEANUP; data?: IBaseJob } + | { name: JobName.TagCleanup; data?: IBaseJob } // Asset Deletion - | { name: JobName.PERSON_CLEANUP; data?: IBaseJob } - | { name: JobName.ASSET_DELETION; data: IAssetDeleteJob } - | { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob } + | { name: JobName.PersonCleanup; data?: IBaseJob } + | { name: JobName.AssetDelete; data: IAssetDeleteJob } + | { name: JobName.AssetDeleteCheck; data?: IBaseJob } // Library Management - | { name: JobName.LIBRARY_SYNC_FILES; data: ILibraryFileJob } - | { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob } - | { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob } - | { name: JobName.LIBRARY_SYNC_ASSETS; data: ILibraryBulkIdsJob } - | { name: JobName.LIBRARY_ASSET_REMOVAL; data: ILibraryFileJob } - | { name: JobName.LIBRARY_DELETE; data: IEntityJob } - | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data?: IBaseJob } - | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } + | { name: JobName.LibrarySyncFiles; data: ILibraryFileJob } + | { name: JobName.LibrarySyncFilesQueueAll; data: IEntityJob } + | { name: JobName.LibrarySyncAssetsQueueAll; data: IEntityJob } + | { name: JobName.LibrarySyncAssets; data: ILibraryBulkIdsJob } + | { name: JobName.LibraryRemoveAsset; data: ILibraryFileJob } + | { name: JobName.LibraryDelete; data: IEntityJob } + | { name: JobName.LibraryScanQueueAll; data?: IBaseJob } + | { name: JobName.LibraryDeleteCheck; data: IBaseJob } // Notification - | { name: JobName.SEND_EMAIL; data: IEmailJob } - | { name: JobName.NOTIFY_ALBUM_INVITE; data: INotifyAlbumInviteJob } - | { name: JobName.NOTIFY_ALBUM_UPDATE; data: INotifyAlbumUpdateJob } - | { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob } + | { name: JobName.SendMail; data: IEmailJob } + | { name: JobName.NotifyAlbumInvite; data: INotifyAlbumInviteJob } + | { name: JobName.NotifyAlbumUpdate; data: INotifyAlbumUpdateJob } + | { name: JobName.NotifyUserSignup; data: INotifySignupJob } // Version check - | { name: JobName.VERSION_CHECK; data: IBaseJob }; + | { name: JobName.VersionCheck; data: IBaseJob }; export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number]; @@ -442,7 +442,7 @@ export type StorageAsset = { export type OnThisDayData = { year: number }; export interface MemoryData { - [MemoryType.ON_THIS_DAY]: OnThisDayData; + [MemoryType.OnThisDay]: OnThisDayData; } export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; @@ -451,16 +451,18 @@ export type MemoriesState = { /** memories have already been created through this date */ lastOnThisDayDate: string; }; +export type MediaLocation = { location: string }; export interface SystemMetadata extends Record> { - [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean }; - [SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string }; - [SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; - [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; - [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial; - [SystemMetadataKey.SYSTEM_FLAGS]: DeepPartial; - [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; - [SystemMetadataKey.MEMORIES_STATE]: MemoriesState; + [SystemMetadataKey.AdminOnboarding]: { isOnboarded: boolean }; + [SystemMetadataKey.FacialRecognitionState]: { lastRun?: string }; + [SystemMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: Date }; + [SystemMetadataKey.MediaLocation]: MediaLocation; + [SystemMetadataKey.ReverseGeocodingState]: { lastUpdate?: string; lastImportFileName?: string }; + [SystemMetadataKey.SystemConfig]: DeepPartial; + [SystemMetadataKey.SystemFlags]: DeepPartial; + [SystemMetadataKey.VersionCheckState]: VersionCheckMetadata; + [SystemMetadataKey.MemoriesState]: MemoriesState; } export type UserMetadataItem = { @@ -513,7 +515,7 @@ export interface UserPreferences { } export interface UserMetadata extends Record> { - [UserMetadataKey.PREFERENCES]: DeepPartial; - [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; - [UserMetadataKey.ONBOARDING]: { isOnboarded: boolean }; + [UserMetadataKey.Preferences]: DeepPartial; + [UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string }; + [UserMetadataKey.Onboarding]: { isOnboarded: boolean }; } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index b639643b6f..08ff81e840 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -11,7 +11,7 @@ export type GrantedRequest = { }; export const isGranted = ({ requested, current }: GrantedRequest) => { - if (current.includes(Permission.ALL)) { + if (current.includes(Permission.All)) { return true; } @@ -63,36 +63,36 @@ const checkSharedLinkAccess = async ( const sharedLinkId = sharedLink.id; switch (permission) { - case Permission.ASSET_READ: { + case Permission.AssetRead: { return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); } - case Permission.ASSET_VIEW: { + case Permission.AssetView: { return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); } - case Permission.ASSET_DOWNLOAD: { + case Permission.AssetDownload: { return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); } - case Permission.ASSET_UPLOAD: { + case Permission.AssetUpload: { return sharedLink.allowUpload ? ids : new Set(); } - case Permission.ASSET_SHARE: { + case Permission.AssetShare: { // TODO: fix this to not use sharedLink.userId for access control return await access.asset.checkOwnerAccess(sharedLink.userId, ids, false); } - case Permission.ALBUM_READ: { + case Permission.AlbumRead: { return await access.album.checkSharedLinkAccess(sharedLinkId, ids); } - case Permission.ALBUM_DOWNLOAD: { + case Permission.AlbumDownload: { return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); } - case Permission.ALBUM_ADD_ASSET: { + case Permission.AlbumAddAsset: { return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); } @@ -107,190 +107,190 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe switch (permission) { // uses album id - case Permission.ACTIVITY_CREATE: { + case Permission.ActivityCreate: { return await access.activity.checkCreateAccess(auth.user.id, ids); } // uses activity id - case Permission.ACTIVITY_DELETE: { + case Permission.ActivityDelete: { const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids); const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isAlbumOwner); } - case Permission.ASSET_READ: { + case Permission.AssetRead: { const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } - case Permission.ASSET_SHARE: { + case Permission.AssetShare: { const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } - case Permission.ASSET_VIEW: { + case Permission.AssetView: { const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } - case Permission.ASSET_DOWNLOAD: { + case Permission.AssetDownload: { const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } - case Permission.ASSET_UPDATE: { + case Permission.AssetUpdate: { return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); } - case Permission.ASSET_DELETE: { + case Permission.AssetDelete: { return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); } - case Permission.ALBUM_READ: { + case Permission.AlbumRead: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - AlbumUserRole.VIEWER, + AlbumUserRole.Viewer, ); return setUnion(isOwner, isShared); } - case Permission.ALBUM_ADD_ASSET: { + case Permission.AlbumAddAsset: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - AlbumUserRole.EDITOR, + AlbumUserRole.Editor, ); return setUnion(isOwner, isShared); } - case Permission.ALBUM_UPDATE: { + case Permission.AlbumUpdate: { return await access.album.checkOwnerAccess(auth.user.id, ids); } - case Permission.ALBUM_DELETE: { + case Permission.AlbumDelete: { return await access.album.checkOwnerAccess(auth.user.id, ids); } - case Permission.ALBUM_SHARE: { + case Permission.AlbumShare: { return await access.album.checkOwnerAccess(auth.user.id, ids); } - case Permission.ALBUM_DOWNLOAD: { + case Permission.AlbumDownload: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - AlbumUserRole.VIEWER, + AlbumUserRole.Viewer, ); return setUnion(isOwner, isShared); } - case Permission.ALBUM_REMOVE_ASSET: { + case Permission.AlbumRemoveAsset: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( auth.user.id, setDifference(ids, isOwner), - AlbumUserRole.EDITOR, + AlbumUserRole.Editor, ); return setUnion(isOwner, isShared); } - case Permission.ASSET_UPLOAD: { + case Permission.AssetUpload: { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); } - case Permission.ARCHIVE_READ: { + case Permission.ArchiveRead: { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); } - case Permission.AUTH_DEVICE_DELETE: { + case Permission.AuthDeviceDelete: { return await access.authDevice.checkOwnerAccess(auth.user.id, ids); } - case Permission.FACE_DELETE: { + case Permission.FaceDelete: { return access.person.checkFaceOwnerAccess(auth.user.id, ids); } - case Permission.NOTIFICATION_READ: - case Permission.NOTIFICATION_UPDATE: - case Permission.NOTIFICATION_DELETE: { + case Permission.NotificationRead: + case Permission.NotificationUpdate: + case Permission.NotificationDelete: { return access.notification.checkOwnerAccess(auth.user.id, ids); } - case Permission.TAG_ASSET: - case Permission.TAG_READ: - case Permission.TAG_UPDATE: - case Permission.TAG_DELETE: { + case Permission.TagAsset: + case Permission.TagRead: + case Permission.TagUpdate: + case Permission.TagDelete: { return await access.tag.checkOwnerAccess(auth.user.id, ids); } - case Permission.TIMELINE_READ: { + case Permission.TimelineRead: { const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } - case Permission.TIMELINE_DOWNLOAD: { + case Permission.TimelineDownload: { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); } - case Permission.MEMORY_READ: { + case Permission.MemoryRead: { return access.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.MEMORY_UPDATE: { + case Permission.MemoryUpdate: { return access.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.MEMORY_DELETE: { + case Permission.MemoryDelete: { return access.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.PERSON_CREATE: { + case Permission.PersonCreate: { return access.person.checkFaceOwnerAccess(auth.user.id, ids); } - case Permission.PERSON_READ: - case Permission.PERSON_UPDATE: - case Permission.PERSON_DELETE: - case Permission.PERSON_MERGE: { + case Permission.PersonRead: + case Permission.PersonUpdate: + case Permission.PersonDelete: + case Permission.PersonMerge: { return await access.person.checkOwnerAccess(auth.user.id, ids); } - case Permission.PERSON_REASSIGN: { + case Permission.PersonReassign: { return access.person.checkFaceOwnerAccess(auth.user.id, ids); } - case Permission.PARTNER_UPDATE: { + case Permission.PartnerUpdate: { return await access.partner.checkUpdateAccess(auth.user.id, ids); } - case Permission.SESSION_READ: - case Permission.SESSION_UPDATE: - case Permission.SESSION_DELETE: - case Permission.SESSION_LOCK: { + case Permission.SessionRead: + case Permission.SessionUpdate: + case Permission.SessionDelete: + case Permission.SessionLock: { return access.session.checkOwnerAccess(auth.user.id, ids); } - case Permission.STACK_READ: { + case Permission.StackRead: { return access.stack.checkOwnerAccess(auth.user.id, ids); } - case Permission.STACK_UPDATE: { + case Permission.StackUpdate: { return access.stack.checkOwnerAccess(auth.user.id, ids); } - case Permission.STACK_DELETE: { + case Permission.StackDelete: { return access.stack.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 85bc6cd2e5..1b9e12c1cd 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -18,9 +18,9 @@ export const getAssetFile = (files: AssetFile[], type: AssetFileType | Generated }; export const getAssetFiles = (files: AssetFile[]) => ({ - fullsizeFile: getAssetFile(files, AssetFileType.FULLSIZE), - previewFile: getAssetFile(files, AssetFileType.PREVIEW), - thumbnailFile: getAssetFile(files, AssetFileType.THUMBNAIL), + fullsizeFile: getAssetFile(files, AssetFileType.FullSize), + previewFile: getAssetFile(files, AssetFileType.Preview), + thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail), }); export const addAssets = async ( @@ -33,7 +33,7 @@ export const addAssets = async ( const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); const allowedAssetIds = await checkAccess(access, { auth, - permission: Permission.ASSET_SHARE, + permission: Permission.AssetShare, ids: notPresentAssetIds, }); @@ -75,7 +75,7 @@ export const removeAssets = async ( const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const allowedAssetIds = canAlwaysRemove.has(dto.parentId) ? existingAssetIds - : await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds }); + : await checkAccess(access, { auth, permission: Permission.AssetShare, ids: existingAssetIds }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -143,15 +143,15 @@ export const onBeforeLink = async ( if (!motionAsset) { throw new BadRequestException('Live photo video not found'); } - if (motionAsset.type !== AssetType.VIDEO) { + if (motionAsset.type !== AssetType.Video) { throw new BadRequestException('Live photo video must be a video'); } if (motionAsset.ownerId !== userId) { throw new BadRequestException('Live photo video does not belong to the user'); } - if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) { - await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN }); + if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) { + await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden }); await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId }); } }; diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index bc1d2dae1b..a669af31cf 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -60,7 +60,7 @@ export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Pr _.set(partialConfig, property, newValue); } - await metadataRepo.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig); + await metadataRepo.set(SystemMetadataKey.SystemConfig, partialConfig); return getConfig(repos, { withCache: false }); }; @@ -83,7 +83,7 @@ const buildConfig = async (repos: RepoDeps) => { // load partial const partial = configFile ? await loadFromFile(repos, configFile) - : await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG); + : await metadataRepo.get(SystemMetadataKey.SystemConfig); // merge with defaults const rawConfig = _.cloneDeep(defaults); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index f23a3deb35..1ef9b8e926 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -154,7 +154,7 @@ export function toJson(qb: SelectQueryBuilder) { - return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); + return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]); } // TODO come up with a better query that only selects the fields we need @@ -299,7 +299,7 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuilderOptions) { options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); - const visibility = options.visibility == null ? AssetVisibility.TIMELINE : options.visibility; + const visibility = options.visibility == null ? AssetVisibility.Timeline : options.visibility; return kysely .withPlugin(joinDeduplicationPlugin) @@ -399,7 +399,7 @@ type VectorIndexQueryOptions = { table: string; vectorExtension: VectorExtension export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: VectorIndexQueryOptions): string { switch (vectorExtension) { - case DatabaseExtension.VECTORCHORD: { + case DatabaseExtension.VectorChord: { return ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vchordrq (embedding vector_cosine_ops) WITH (options = $$ residual_quantization = false @@ -410,7 +410,7 @@ export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: V sampling_factor = 1024 $$)`; } - case DatabaseExtension.VECTORS: { + case DatabaseExtension.Vectors: { return ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vectors (embedding vector_cos_ops) WITH (options = $$ @@ -420,7 +420,7 @@ export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: V ef_construction = 300 $$)`; } - case DatabaseExtension.VECTOR: { + case DatabaseExtension.Vector: { return ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING hnsw (embedding vector_cosine_ops) diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 716e0b1957..2331a45a62 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -1,7 +1,7 @@ import { HttpException, StreamableFile } from '@nestjs/common'; import { NextFunction, Response } from 'express'; import { access, constants } from 'node:fs/promises'; -import { basename, extname, isAbsolute } from 'node:path'; +import { basename, extname } from 'node:path'; import { promisify } from 'node:util'; import { CacheControl } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -34,9 +34,9 @@ type SendFile = Parameters; type SendFileOptions = SendFile[1]; const cacheControlHeaders: Record = { - [CacheControl.PRIVATE_WITH_CACHE]: 'private, max-age=86400, no-transform', - [CacheControl.PRIVATE_WITHOUT_CACHE]: 'private, no-cache, no-transform', - [CacheControl.NONE]: null, // falsy value to prevent adding Cache-Control header + [CacheControl.PrivateWithCache]: 'private, max-age=86400, no-transform', + [CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform', + [CacheControl.None]: null, // falsy value to prevent adding Cache-Control header }; export const sendFile = async ( @@ -62,15 +62,9 @@ export const sendFile = async ( res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`); } - // configure options for serving - const options: SendFileOptions = { dotfiles: 'allow' }; - if (!isAbsolute(file.path)) { - options.root = process.cwd(); - } - await access(file.path, constants.R_OK); - return await _sendFile(file.path, options); + return await _sendFile(file.path, { dotfiles: 'allow' }); } catch (error: Error | any) { // ignore client-closed connection if (isConnectionAborted(error) || res.headersSent) { diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index b00eb652ef..e43ecba49f 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,5 +1,5 @@ import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; -import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum'; +import { CQMode, ToneMapping, TranscodeHardwareAcceleration, TranscodeTarget, VideoCodec } from 'src/enum'; import { AudioStreamInfo, BitrateDistribution, @@ -16,7 +16,7 @@ export class BaseConfig implements VideoCodecSWConfig { protected constructor(protected config: SystemConfigFFmpegDto) {} static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces): VideoCodecSWConfig { - if (config.accel === TranscodeHWAccel.DISABLED) { + if (config.accel === TranscodeHardwareAcceleration.Disabled) { return this.getSWCodecConfig(config); } return this.getHWCodecConfig(config, interfaces); @@ -27,13 +27,13 @@ export class BaseConfig implements VideoCodecSWConfig { case VideoCodec.H264: { return new H264Config(config); } - case VideoCodec.HEVC: { + case VideoCodec.Hevc: { return new HEVCConfig(config); } - case VideoCodec.VP9: { + case VideoCodec.Vp9: { return new VP9Config(config); } - case VideoCodec.AV1: { + case VideoCodec.Av1: { return new AV1Config(config); } default: { @@ -45,25 +45,25 @@ export class BaseConfig implements VideoCodecSWConfig { private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces) { let handler: VideoCodecHWConfig; switch (config.accel) { - case TranscodeHWAccel.NVENC: { + case TranscodeHardwareAcceleration.Nvenc: { handler = config.accelDecode ? new NvencHwDecodeConfig(config, interfaces) : new NvencSwDecodeConfig(config, interfaces); break; } - case TranscodeHWAccel.QSV: { + case TranscodeHardwareAcceleration.Qsv: { handler = config.accelDecode ? new QsvHwDecodeConfig(config, interfaces) : new QsvSwDecodeConfig(config, interfaces); break; } - case TranscodeHWAccel.VAAPI: { + case TranscodeHardwareAcceleration.Vaapi: { handler = config.accelDecode ? new VaapiHwDecodeConfig(config, interfaces) : new VaapiSwDecodeConfig(config, interfaces); break; } - case TranscodeHWAccel.RKMPP: { + case TranscodeHardwareAcceleration.Rkmpp: { handler = config.accelDecode ? new RkmppHwDecodeConfig(config, interfaces) : new RkmppSwDecodeConfig(config, interfaces); @@ -94,7 +94,7 @@ export class BaseConfig implements VideoCodecSWConfig { twoPass: this.eligibleForTwoPass(), progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, } as TranscodeCommand; - if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) { + if ([TranscodeTarget.All, TranscodeTarget.Video].includes(target)) { const filters = this.getFilterOptions(videoStream); if (filters.length > 0) { options.outputOptions.push(`-vf ${filters.join(',')}`); @@ -116,8 +116,8 @@ export class BaseConfig implements VideoCodecSWConfig { } getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { - const videoCodec = [TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target) ? this.getVideoCodec() : 'copy'; - const audioCodec = [TranscodeTarget.ALL, TranscodeTarget.AUDIO].includes(target) ? this.getAudioCodec() : 'copy'; + const videoCodec = [TranscodeTarget.All, TranscodeTarget.Video].includes(target) ? this.getVideoCodec() : 'copy'; + const audioCodec = [TranscodeTarget.All, TranscodeTarget.Audio].includes(target) ? this.getAudioCodec() : 'copy'; const options = [ `-c:v ${videoCodec}`, @@ -146,7 +146,7 @@ export class BaseConfig implements VideoCodecSWConfig { } if ( - this.config.targetVideoCodec === VideoCodec.HEVC && + this.config.targetVideoCodec === VideoCodec.Hevc && (videoCodec !== 'copy' || videoStream.codecName === 'hevc') ) { options.push('-tag:v hvc1'); @@ -207,7 +207,7 @@ export class BaseConfig implements VideoCodecSWConfig { } eligibleForTwoPass() { - if (!this.config.twoPass || this.config.accel !== TranscodeHWAccel.DISABLED) { + if (!this.config.twoPass || this.config.accel !== TranscodeHardwareAcceleration.Disabled) { return false; } @@ -244,7 +244,7 @@ export class BaseConfig implements VideoCodecSWConfig { } shouldToneMap(videoStream: VideoStreamInfo) { - return videoStream.isHDR && this.config.tonemap !== ToneMapping.DISABLED; + return videoStream.isHDR && this.config.tonemap !== ToneMapping.Disabled; } getScaling(videoStream: VideoStreamInfo, mult = 2) { @@ -326,7 +326,7 @@ export class BaseConfig implements VideoCodecSWConfig { } useCQP() { - return this.config.cqMode === CQMode.CQP; + return this.config.cqMode === CQMode.Cqp; } } @@ -344,7 +344,7 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC]; + return [VideoCodec.H264, VideoCodec.Hevc]; } validateDevices(devices: string[]) { @@ -526,7 +526,7 @@ export class NvencSwDecodeConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; + return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Av1]; } getBaseInputOptions() { @@ -658,7 +658,7 @@ export class QsvSwDecodeConfig extends BaseHWConfig { getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { const options = super.getBaseOutputOptions(target, videoStream, audioStream); // VP9 requires enabling low power mode https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/33583803e107b6d532def0f9d949364b01b6ad5a - if (this.config.targetVideoCodec === VideoCodec.VP9) { + if (this.config.targetVideoCodec === VideoCodec.Vp9) { options.push('-low_power 1'); } return options; @@ -693,7 +693,7 @@ export class QsvSwDecodeConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1]; + return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; } // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md @@ -712,7 +712,7 @@ export class QsvSwDecodeConfig extends BaseHWConfig { } useCQP() { - return this.config.cqMode === CQMode.CQP || this.config.targetVideoCodec === VideoCodec.VP9; + return this.config.cqMode === CQMode.Cqp || this.config.targetVideoCodec === VideoCodec.Vp9; } getScaling(videoStream: VideoStreamInfo): string { @@ -802,7 +802,7 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { const bitrates = this.getBitrateDistribution(); const options = []; - if (this.config.targetVideoCodec === VideoCodec.VP9) { + if (this.config.targetVideoCodec === VideoCodec.Vp9) { options.push('-bsf:v vp9_raw_reorder,vp9_superframe'); } @@ -824,11 +824,11 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1]; + return [VideoCodec.H264, VideoCodec.Hevc, VideoCodec.Vp9, VideoCodec.Av1]; } useCQP() { - return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9; + return this.config.cqMode !== CQMode.Icq || this.config.targetVideoCodec === VideoCodec.Vp9; } } @@ -900,7 +900,7 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { // from ffmpeg_mpp help, commonly referred to as H264 level 5.1 return ['-level 51']; } - case VideoCodec.HEVC: { + case VideoCodec.Hevc: { // from ffmpeg_mpp help, commonly referred to as HEVC level 5.1 return ['-level 153']; } @@ -921,7 +921,7 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC]; + return [VideoCodec.H264, VideoCodec.Hevc]; } getVideoCodec(): string { diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 6aad418d9f..6b9392146d 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -129,11 +129,11 @@ export const mimeTypes = { assetType: (filename: string) => { const contentType = lookup(filename); if (contentType.startsWith('image/')) { - return AssetType.IMAGE; + return AssetType.Image; } else if (contentType.startsWith('video/')) { - return AssetType.VIDEO; + return AssetType.Video; } - return AssetType.OTHER; + return AssetType.Other; }, getSupportedFileExtensions: () => [...Object.keys(image), ...Object.keys(video)], }; diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 742e98c1c3..3acb72b663 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -234,14 +234,14 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) scheme: 'Bearer', in: 'header', }) - .addCookieAuth(ImmichCookie.ACCESS_TOKEN) + .addCookieAuth(ImmichCookie.AccessToken) .addApiKey( { type: 'apiKey', in: 'header', - name: ImmichHeader.API_KEY, + name: ImmichHeader.ApiKey, }, - MetadataKey.API_KEY_SECURITY, + MetadataKey.ApiKeySecurity, ) .addServer('/api') .build(); diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index 9bd3dedd52..121bf2826d 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -8,7 +8,7 @@ import { getKeysDeep } from 'src/utils/misc'; const getDefaultPreferences = (): UserPreferences => { return { albums: { - defaultAssetOrder: AssetOrder.DESC, + defaultAssetOrder: AssetOrder.Desc, }, folders: { enabled: false, @@ -53,7 +53,7 @@ const getDefaultPreferences = (): UserPreferences => { export const getPreferences = (metadata: UserMetadataItem[]): UserPreferences => { const preferences = getDefaultPreferences(); - const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES); + const item = metadata.find(({ key }) => key === UserMetadataKey.Preferences); const partial = item?.value || {}; for (const property of getKeysDeep(partial)) { _.set(preferences, property, _.get(partial, property)); diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts index a50e86a4ff..c5f51c385c 100644 --- a/server/src/utils/response.ts +++ b/server/src/utils/response.ts @@ -13,13 +13,13 @@ export const respondWithCookie = (res: Response, body: T, { isSecure, values }; const cookieOptions: Record = { - [ImmichCookie.AUTH_TYPE]: defaults, - [ImmichCookie.ACCESS_TOKEN]: defaults, - [ImmichCookie.OAUTH_STATE]: defaults, - [ImmichCookie.OAUTH_CODE_VERIFIER]: defaults, + [ImmichCookie.AuthType]: defaults, + [ImmichCookie.AccessToken]: defaults, + [ImmichCookie.OAuthState]: defaults, + [ImmichCookie.OAuthCodeVerifier]: defaults, // no httpOnly so that the client can know the auth state - [ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false }, - [ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, + [ImmichCookie.IsAuthenticated]: { ...defaults, httpOnly: false }, + [ImmichCookie.SharedLinkToken]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, }; for (const { key, value } of values) { diff --git a/server/start.sh b/server/start.sh deleted file mode 100755 index 1a08d01a75..0000000000 --- a/server/start.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -echo "Initializing Immich $IMMICH_SOURCE_REF" - -lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2" -export LD_PRELOAD="$lib_path" -export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib" - -read_file_and_export() { - if [ -n "${!1}" ]; then - content="$(cat "${!1}")" - export "$2"="${content}" - unset "$1" - fi -} -read_file_and_export "DB_URL_FILE" "DB_URL" -read_file_and_export "DB_HOSTNAME_FILE" "DB_HOSTNAME" -read_file_and_export "DB_DATABASE_NAME_FILE" "DB_DATABASE_NAME" -read_file_and_export "DB_USERNAME_FILE" "DB_USERNAME" -read_file_and_export "DB_PASSWORD_FILE" "DB_PASSWORD" -read_file_and_export "REDIS_PASSWORD_FILE" "REDIS_PASSWORD" - -export CPU_CORES="${CPU_CORES:=$(./get-cpus.sh)}" -echo "Detected CPU Cores: $CPU_CORES" -if [ "$CPU_CORES" -gt 4 ]; then - export UV_THREADPOOL_SIZE=$CPU_CORES -fi - -exec node /usr/src/app/dist/main "$@" diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index fd6a8678a0..d36989bbcf 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -19,7 +19,7 @@ export const albumStub = { sharedLinks: [], albumUsers: [], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), sharedWithUser: Object.freeze({ @@ -38,11 +38,11 @@ export const albumStub = { albumUsers: [ { user: userStub.user1, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }, ], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), sharedWithMultiple: Object.freeze({ @@ -61,15 +61,15 @@ export const albumStub = { albumUsers: [ { user: userStub.user1, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }, { user: userStub.user2, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }, ], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), sharedWithAdmin: Object.freeze({ @@ -88,11 +88,11 @@ export const albumStub = { albumUsers: [ { user: userStub.admin, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }, ], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), oneAsset: Object.freeze({ @@ -110,7 +110,7 @@ export const albumStub = { sharedLinks: [], albumUsers: [], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), twoAssets: Object.freeze({ @@ -128,7 +128,7 @@ export const albumStub = { sharedLinks: [], albumUsers: [], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), emptyWithValidThumbnail: Object.freeze({ @@ -146,7 +146,7 @@ export const albumStub = { sharedLinks: [], albumUsers: [], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, updateId: '42', }), }; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index aa38a520ee..991c5d2c4f 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -8,19 +8,19 @@ import { userStub } from 'test/fixtures/user.stub'; export const previewFile: AssetFile = { id: 'file-1', - type: AssetFileType.PREVIEW, + type: AssetFileType.Preview, path: '/uploads/user-id/thumbs/path.jpg', }; const thumbnailFile: AssetFile = { id: 'file-2', - type: AssetFileType.THUMBNAIL, + type: AssetFileType.Thumbnail, path: '/uploads/user-id/webp/path.ext', }; const fullsizeFile: AssetFile = { id: 'file-3', - type: AssetFileType.FULLSIZE, + type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/path.webp', }; @@ -44,7 +44,7 @@ export const assetStub = { id: 'asset-id', ownerId: 'user-id', livePhotoVideoId: null, - type: AssetType.IMAGE, + type: AssetType.Image, isExternal: false, checksum: Buffer.from('file hash'), timeZone: null, @@ -57,7 +57,7 @@ export const assetStub = { }), noResizePath: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, originalFileName: 'IMG_123.jpg', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -68,7 +68,7 @@ export const assetStub = { originalPath: 'upload/library/IMG_123.jpg', files: [thumbnailFile], checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -89,12 +89,12 @@ export const assetStub = { libraryId: null, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), noWebpPath: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -104,7 +104,7 @@ export const assetStub = { originalPath: 'upload/library/IMG_456.jpg', files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -128,12 +128,12 @@ export const assetStub = { libraryId: null, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), noThumbhash: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -143,7 +143,7 @@ export const assetStub = { originalPath: '/original/path.ext', files, checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -164,12 +164,12 @@ export const assetStub = { libraryId: null, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), primaryImage: Object.freeze({ id: 'primary-asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -179,7 +179,7 @@ export const assetStub = { originalPath: '/original/path.jpg', checksum: Buffer.from('file hash', 'utf8'), files, - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -210,12 +210,12 @@ export const assetStub = { isOffline: false, updateId: '42', libraryId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), image: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -225,7 +225,7 @@ export const assetStub = { originalPath: '/original/path.jpg', files, checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -256,7 +256,7 @@ export const assetStub = { projectionType: null, height: 3840, width: 2160, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), trashed: Object.freeze({ @@ -269,7 +269,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.jpg', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -293,16 +293,16 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, - status: AssetStatus.TRASHED, + status: AssetStatus.Trashed, libraryId: null, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), trashedOffline: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -311,7 +311,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.jpg', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -338,11 +338,11 @@ export const assetStub = { isOffline: true, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), archived: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -351,7 +351,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.jpg', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -378,12 +378,12 @@ export const assetStub = { libraryId: null, stackId: null, updateId: '42', - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), external: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -392,7 +392,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', checksum: Buffer.from('path hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -418,12 +418,12 @@ export const assetStub = { updateId: '42', stackId: null, stack: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), image1: Object.freeze({ id: 'asset-id-1', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -432,7 +432,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.ext', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -458,12 +458,12 @@ export const assetStub = { stackId: null, libraryId: null, stack: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), imageFrom2015: Object.freeze({ id: 'asset-id-1', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'), @@ -473,7 +473,7 @@ export const assetStub = { originalPath: '/original/path.ext', files, checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -494,12 +494,12 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), video: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, originalFileName: 'asset-id.ext', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -509,7 +509,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.ext', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.VIDEO, + type: AssetType.Video, files: [previewFile], thumbhash: null, encodedVideoPath: null, @@ -535,15 +535,15 @@ export const assetStub = { updateId: '42', libraryId: null, stackId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), livePhotoMotionAsset: Object.freeze({ - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, id: fileStub.livePhotoMotion.uuid, originalPath: fileStub.livePhotoMotion.originalPath, ownerId: authStub.user1.user.id, - type: AssetType.VIDEO, + type: AssetType.Video, fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), exifInfo: { @@ -551,15 +551,15 @@ export const assetStub = { timeZone: `America/New_York`, }, libraryId: null, - visibility: AssetVisibility.HIDDEN, + visibility: AssetVisibility.Hidden, } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), livePhotoStillAsset: Object.freeze({ id: 'live-photo-still-asset', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, originalPath: fileStub.livePhotoStill.originalPath, ownerId: authStub.user1.user.id, - type: AssetType.IMAGE, + type: AssetType.Image, livePhotoVideoId: 'live-photo-motion-asset', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -569,16 +569,16 @@ export const assetStub = { }, files, faces: [] as AssetFace[], - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, } as MapAsset & { faces: AssetFace[] }), livePhotoWithOriginalFileName: Object.freeze({ id: 'live-photo-still-asset', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, originalPath: fileStub.livePhotoStill.originalPath, originalFileName: fileStub.livePhotoStill.originalName, ownerId: authStub.user1.user.id, - type: AssetType.IMAGE, + type: AssetType.Image, livePhotoVideoId: 'live-photo-motion-asset', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -588,12 +588,12 @@ export const assetStub = { }, libraryId: null, faces: [] as AssetFace[], - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, } as MapAsset & { faces: AssetFace[] }), withLocation: Object.freeze({ id: 'asset-with-favorite-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'), @@ -603,7 +603,7 @@ export const assetStub = { checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', sidecarPath: null, - type: AssetType.IMAGE, + type: AssetType.Image, files: [previewFile], thumbhash: null, encodedVideoPath: null, @@ -633,12 +633,12 @@ export const assetStub = { duplicateId: null, isOffline: false, tags: [], - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), sidecar: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -648,7 +648,7 @@ export const assetStub = { originalPath: '/original/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -669,12 +669,12 @@ export const assetStub = { updateId: 'foo', libraryId: null, stackId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), sidecarWithoutExt: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -684,7 +684,7 @@ export const assetStub = { originalPath: '/original/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -702,12 +702,12 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), hasEncodedVideo: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, originalFileName: 'asset-id.ext', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -717,7 +717,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.ext', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.VIDEO, + type: AssetType.Video, files: [previewFile], thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', @@ -742,12 +742,12 @@ export const assetStub = { libraryId: null, stackId: null, stack: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), hasFileExtension: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -756,7 +756,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -779,12 +779,12 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), imageDng: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -793,7 +793,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.dng', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -820,12 +820,12 @@ export const assetStub = { updateId: '42', libraryId: null, stackId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), imageHif: Object.freeze({ id: 'asset-id', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -834,7 +834,7 @@ export const assetStub = { deviceId: 'device-id', originalPath: '/original/path.hif', checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, + type: AssetType.Image, files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, @@ -861,6 +861,6 @@ export const assetStub = { updateId: '42', libraryId: null, stackId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }), }; diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index fe5cbb9a56..f655a3944e 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -20,9 +20,11 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, faceSearch: { faceId: 'assetFaceId1', embedding: '[1, 2, 3, 4]' }, deletedAt: new Date(), + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), primaryFace1: Object.freeze({ id: 'assetFaceId2', @@ -36,9 +38,11 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, faceSearch: { faceId: 'assetFaceId2', embedding: '[1, 2, 3, 4]' }, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), mergeFace1: Object.freeze({ id: 'assetFaceId3', @@ -52,9 +56,11 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, faceSearch: { faceId: 'assetFaceId3', embedding: '[1, 2, 3, 4]' }, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), noPerson1: Object.freeze({ id: 'assetFaceId8', @@ -68,9 +74,11 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, faceSearch: { faceId: 'assetFaceId8', embedding: '[1, 2, 3, 4]' }, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), noPerson2: Object.freeze({ id: 'assetFaceId9', @@ -84,9 +92,11 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, faceSearch: { faceId: 'assetFaceId9', embedding: '[1, 2, 3, 4]' }, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), fromExif1: Object.freeze({ id: 'assetFaceId9', @@ -100,8 +110,10 @@ export const faceStub = { boundingBoxY2: 200, imageHeight: 500, imageWidth: 400, - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), fromExif2: Object.freeze({ id: 'assetFaceId9', @@ -115,8 +127,10 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.EXIF, + sourceType: SourceType.Exif, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), withBirthDate: Object.freeze({ id: 'assetFaceId10', @@ -130,7 +144,9 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, - sourceType: SourceType.MACHINE_LEARNING, + sourceType: SourceType.MachineLearning, deletedAt: null, + updatedAt: new Date('2023-01-01T00:00:00Z'), + updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', }), }; diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 86f3bcde21..35a7a8ed7d 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -176,7 +176,7 @@ export const personThumbnailStub = { y2: 505, oldHeight: 2880, oldWidth: 2160, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', previewPath: previewFile.path, @@ -189,7 +189,7 @@ export const personThumbnailStub = { y2: 200, oldHeight: 500, oldWidth: 400, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', previewPath: previewFile.path, @@ -202,7 +202,7 @@ export const personThumbnailStub = { y2: 495, oldHeight: 500, oldWidth: 500, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', previewPath: previewFile.path, @@ -215,7 +215,7 @@ export const personThumbnailStub = { y2: 200, oldHeight: 500, oldWidth: 400, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.dng', exifOrientation: '1', previewPath: previewFile.path, @@ -228,7 +228,7 @@ export const personThumbnailStub = { y2: 251, oldHeight: 1440, oldWidth: 2162, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', previewPath: previewFile.path, @@ -241,7 +241,7 @@ export const personThumbnailStub = { y2: 152, oldHeight: 1440, oldWidth: 2162, - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', previewPath: previewFile.path, @@ -254,7 +254,7 @@ export const personThumbnailStub = { y2: 200, oldHeight: 500, oldWidth: 400, - type: AssetType.VIDEO, + type: AssetType.Video, originalPath: '/original/path.mp4', exifOrientation: '1', previewPath: previewFile.path, diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index f3096280d9..47201a5b3b 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -49,7 +49,7 @@ const assetResponse: AssetResponseDto = { deviceAssetId: 'device_asset_id_1', ownerId: 'user_id_1', deviceId: 'device_id_1', - type: AssetType.VIDEO, + type: AssetType.Video, originalMimeType: 'image/jpeg', originalPath: 'fake_path/jpeg', originalFileName: 'asset_1.jpeg', @@ -70,12 +70,12 @@ const assetResponse: AssetResponseDto = { isTrashed: false, libraryId: 'library-id', hasMetadata: true, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }; const assetResponseWithoutMetadata = { id: 'id_1', - type: AssetType.VIDEO, + type: AssetType.Video, originalMimeType: 'image/jpeg', thumbhash: null, localDateTime: today, @@ -99,7 +99,7 @@ const albumResponse: AlbumResponseDto = { assets: [], assetCount: 1, isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, }; export const sharedLinkStub = { @@ -107,7 +107,7 @@ export const sharedLinkStub = { id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, - type: SharedLinkType.INDIVIDUAL, + type: SharedLinkType.Individual, createdAt: today, expiresAt: tomorrow, allowUpload: true, @@ -124,7 +124,7 @@ export const sharedLinkStub = { userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, createdAt: today, expiresAt: tomorrow, allowUpload: true, @@ -141,7 +141,7 @@ export const sharedLinkStub = { userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, createdAt: today, expiresAt: yesterday, allowUpload: true, @@ -157,7 +157,7 @@ export const sharedLinkStub = { id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, createdAt: today, expiresAt: tomorrow, allowUpload: false, @@ -182,16 +182,16 @@ export const sharedLinkStub = { albumUsers: [], sharedLinks: [], isActivityEnabled: true, - order: AssetOrder.DESC, + order: AssetOrder.Desc, assets: [ { id: 'id_1', - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, owner: undefined as unknown as UserAdmin, ownerId: 'user_id_1', deviceAssetId: 'device_asset_id_1', deviceId: 'device_id_1', - type: AssetType.VIDEO, + type: AssetType.Video, originalPath: 'fake_path/jpeg', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, @@ -251,7 +251,7 @@ export const sharedLinkStub = { updateId: '42', libraryId: null, stackId: null, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }, ], }, @@ -260,7 +260,7 @@ export const sharedLinkStub = { id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, createdAt: today, expiresAt: tomorrow, allowUpload: true, @@ -286,7 +286,7 @@ export const sharedLinkResponseStub = { id: '123', key: sharedLinkBytes.toString('base64url'), showMetadata: true, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, userId: 'admin_id', }), expired: Object.freeze({ @@ -301,14 +301,14 @@ export const sharedLinkResponseStub = { id: '123', key: sharedLinkBytes.toString('base64url'), showMetadata: true, - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, userId: 'admin_id', }), readonlyNoMetadata: Object.freeze({ id: '123', userId: 'admin_id', key: sharedLinkBytes.toString('base64url'), - type: SharedLinkType.ALBUM, + type: SharedLinkType.Album, createdAt: today, expiresAt: tomorrow, description: null, diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 0db58e2eed..807da5197f 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -5,7 +5,7 @@ import { authStub } from 'test/fixtures/auth.stub'; export const userStub = { admin: { ...authStub.admin.user, - status: UserStatus.ACTIVE, + status: UserStatus.Active, profileChangedAt: new Date('2021-01-01'), name: 'admin_name', id: 'admin_id', @@ -23,7 +23,7 @@ export const userStub = { }, user1: { ...authStub.user1.user, - status: UserStatus.ACTIVE, + status: UserStatus.Active, profileChangedAt: new Date('2021-01-01'), name: 'immich_name', storageLabel: null, @@ -40,7 +40,7 @@ export const userStub = { }, user2: { ...authStub.user2.user, - status: UserStatus.ACTIVE, + status: UserStatus.Active, profileChangedAt: new Date('2021-01-01'), metadata: [], name: 'immich_name', diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 9c1032663f..d6038b6b84 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -154,6 +154,12 @@ export class MediumTestContext { return { asset, result }; } + async newAssetFace(dto: Partial> & { assetId: string }) { + const assetFace = mediumFactory.assetFaceInsert(dto); + const result = await this.get(PersonRepository).createAssetFace(assetFace); + return { assetFace, result }; + } + async newMemory(dto: Partial> = {}) { const memory = mediumFactory.memoryInsert(dto); const result = await this.get(MemoryRepository).create(memory, new Set()); @@ -182,7 +188,7 @@ export class MediumTestContext { } async newAlbumUser(dto: { albumId: string; userId: string; role?: AlbumUserRole }) { - const { albumId, userId, role = AlbumUserRole.EDITOR } = dto; + const { albumId, userId, role = AlbumUserRole.Editor } = dto; const result = await this.get(AlbumUserRepository).create({ albumsId: albumId, usersId: userId, role }); return { albumUser: { albumId, userId, role }, result }; } @@ -370,14 +376,14 @@ const assetInsert = (asset: Partial> = {}) => { deviceId: '', originalFileName: '', checksum: randomBytes(32), - type: AssetType.IMAGE, + type: AssetType.Image, originalPath: '/path/to/something.jpg', ownerId: '@immich.cloud', isFavorite: false, fileCreatedAt: now, fileModifiedAt: now, localDateTime: now, - visibility: AssetVisibility.TIMELINE, + visibility: AssetVisibility.Timeline, }; return { @@ -423,7 +429,7 @@ const assetFaceInsert = (assetFace: Partial & { assetId: string }) => imageHeight: assetFace.imageHeight ?? 10, imageWidth: assetFace.imageWidth ?? 10, personId: assetFace.personId ?? null, - sourceType: assetFace.sourceType ?? SourceType.MACHINE_LEARNING, + sourceType: assetFace.sourceType ?? SourceType.MachineLearning, }; return { @@ -516,7 +522,7 @@ const memoryInsert = (memory: Partial> = {}) => { createdAt: date, updatedAt: date, deletedAt: null, - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year: 2025 }, showAt: null, hideAt: null, diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index f1d50ce841..14ea1451f2 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -102,7 +102,7 @@ describe(AuthService.name, () => { it('should logout', async () => { const { sut } = setup(); const auth = factory.auth(); - await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -118,7 +118,7 @@ describe(AuthService.name, () => { eventRepo.emit.mockResolvedValue(); await expect(sessionRepo.get(session.id)).resolves.toEqual(expect.objectContaining({ id: session.id })); - await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index bdd06b4a3f..12df2f130e 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -45,7 +45,7 @@ describe(MemoryService.name, () => { const { user } = await ctx.newUser(); const auth = factory.auth({ user }); const dto = { - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year: 2021 }, memoryAt: new Date(2021), }; @@ -70,7 +70,7 @@ describe(MemoryService.name, () => { const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); const auth = factory.auth({ user }); const dto = { - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year: 2021 }, memoryAt: new Date(2021), assetIds: [asset1.id, asset2.id], @@ -92,7 +92,7 @@ describe(MemoryService.name, () => { const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id }); const auth = factory.auth({ user: user1 }); const dto = { - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year: 2021 }, memoryAt: new Date(2021), assetIds: [asset1.id, asset2.id], @@ -124,8 +124,8 @@ describe(MemoryService.name, () => { ctx.newExif({ assetId: asset.id, make: 'Canon' }), ctx.newJobStatus({ assetId: asset.id }), assetRepo.upsertFiles([ - { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, - { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, + { assetId: asset.id, type: AssetFileType.Preview, path: '/path/to/preview.jpg' }, + { assetId: asset.id, type: AssetFileType.Thumbnail, path: '/path/to/thumbnail.jpg' }, ]), ]); @@ -178,8 +178,8 @@ describe(MemoryService.name, () => { ctx.newExif({ assetId: asset.id, make: 'Canon' }), ctx.newJobStatus({ assetId: asset.id }), assetRepo.upsertFiles([ - { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, - { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, + { assetId: asset.id, type: AssetFileType.Preview, path: '/path/to/preview.jpg' }, + { assetId: asset.id, type: AssetFileType.Thumbnail, path: '/path/to/thumbnail.jpg' }, ]), ]); } diff --git a/server/test/medium/specs/services/timeline.service.spec.ts b/server/test/medium/specs/services/timeline.service.spec.ts index 6af936eb49..fa4a75e869 100644 --- a/server/test/medium/specs/services/timeline.service.spec.ts +++ b/server/test/medium/specs/services/timeline.service.spec.ts @@ -46,7 +46,7 @@ describe(TimelineService.name, () => { it('should return error if time bucket is requested with partners asset and archived', async () => { const { sut } = setup(); const auth = factory.auth(); - const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.ARCHIVE }); + const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Archive }); await expect(response1).rejects.toBeInstanceOf(BadRequestException); await expect(response1).rejects.toThrow( 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 535ac30aec..7643be292e 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -16,7 +16,7 @@ import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = (db?: Kysely) => { - process.env.IMMICH_ENV = ImmichEnvironment.TESTING; + process.env.IMMICH_ENV = ImmichEnvironment.Testing; return newMediumService(UserService, { database: db || defaultDatabase, @@ -140,7 +140,7 @@ describe(UserService.name, () => { const { sut, ctx } = setup(); const jobMock = ctx.getMock(JobRepository); jobMock.queueAll.mockResolvedValue(void 0); - await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.Success); expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); @@ -149,10 +149,8 @@ describe(UserService.name, () => { const jobMock = ctx.getMock(JobRepository); const { user } = await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() }); jobMock.queueAll.mockResolvedValue(void 0); - await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([ - { name: JobName.USER_DELETION, data: { id: user.id } }, - ]); + await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.Success); + expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([{ name: JobName.UserDelete, data: { id: user.id } }]); }); it('should skip a recently deleted user', async () => { @@ -160,7 +158,7 @@ describe(UserService.name, () => { const jobMock = ctx.getMock(JobRepository); await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() }); jobMock.queueAll.mockResolvedValue(void 0); - await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.Success); expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); @@ -172,7 +170,7 @@ describe(UserService.name, () => { const config = await sut.getConfig({ withCache: false }); config.user.deleteDelay = 30; await sut.updateConfig(config); - await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); + await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.Success); expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); }); diff --git a/server/test/medium/specs/services/version.service.spec.ts b/server/test/medium/specs/services/version.service.spec.ts index 9feda5b8c4..3e81429382 100644 --- a/server/test/medium/specs/services/version.service.spec.ts +++ b/server/test/medium/specs/services/version.service.spec.ts @@ -53,7 +53,7 @@ describe(VersionService.name, () => { await versionHistoryRepo.create({ version: 'v1.128.0' }); await sut.onBootstrap(); - expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE }); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.MemoryGenerate }); }); it('should not queue memory creation when upgrading from 1.129.0', async () => { diff --git a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts index 2c6b98e949..808a4785ce 100644 --- a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts @@ -25,7 +25,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); expect(response).toHaveLength(1); @@ -86,7 +86,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.Editor }); const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); @@ -110,7 +110,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { await ctx.newExif({ assetId: asset3User2.id, make: 'asset3User2' }); const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id }); - await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); expect(response).toHaveLength(1); @@ -134,7 +134,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => { ctx.newAlbumAsset({ albumId: album2.id, assetId }), ), ); - await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); // should backfill the album user const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index 41700d29d4..9a42c0f027 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -41,7 +41,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); expect(response).toHaveLength(1); @@ -90,7 +90,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: user3.id }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.Editor }); const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); @@ -111,7 +111,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { await wait(2); const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id }); - await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); expect(response).toHaveLength(1); @@ -135,7 +135,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { ctx.newAlbumAsset({ albumId: album2.id, assetId }), ), ); - await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); // should backfill the album user const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index a0c1d413a0..ee529c5001 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -73,7 +73,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); expect(response).toHaveLength(1); @@ -130,7 +130,7 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { await ctx.syncAckAll(auth, response); // add user to backfill album - await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); // should backfill the album to asset relation const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); diff --git a/server/test/medium/specs/sync/sync-album-user.spec.ts b/server/test/medium/specs/sync/sync-album-user.spec.ts index 798b3d607d..e3d8a21493 100644 --- a/server/test/medium/specs/sync/sync-album-user.spec.ts +++ b/server/test/medium/specs/sync/sync-album-user.spec.ts @@ -22,7 +22,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { auth, ctx } = await setup(); const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); const { user } = await ctx.newUser(); - const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ { @@ -42,7 +42,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { auth, ctx } = await setup(); const { user: user1 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); - const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(1); @@ -67,13 +67,13 @@ describe(SyncRequestType.AlbumUsersV1, () => { const albumUserRepo = ctx.get(AlbumUserRepository); const { user: user1 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); - const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); await ctx.syncAckAll(auth, response); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); - await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER }); + await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(newResponse).toHaveLength(1); expect(newResponse).toEqual([ @@ -81,7 +81,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { ack: expect.any(String), data: expect.objectContaining({ albumId: albumUser.albumId, - role: AlbumUserRole.VIEWER, + role: AlbumUserRole.Viewer, userId: albumUser.userId, }), type: SyncEntityType.AlbumUserV1, @@ -97,7 +97,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { const albumUserRepo = ctx.get(AlbumUserRepository); const { user: user1 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); - const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(1); @@ -130,7 +130,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -157,8 +157,8 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { user: owner } = await ctx.newUser(); const { user: user } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: owner.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); + await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(2); @@ -166,14 +166,14 @@ describe(SyncRequestType.AlbumUsersV1, () => { await ctx.syncAckAll(auth, response); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); - await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER }); + await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.Viewer }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ albumId: album.id, - role: AlbumUserRole.VIEWER, + role: AlbumUserRole.Viewer, userId: user.id, }), type: SyncEntityType.AlbumUserV1, @@ -190,8 +190,8 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { user: owner } = await ctx.newUser(); const { user: user } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: owner.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); + await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(2); @@ -223,13 +223,13 @@ describe(SyncRequestType.AlbumUsersV1, () => { const { album: album1 } = await ctx.newAlbum({ ownerId: user1.id }); const { album: album2 } = await ctx.newAlbum({ ownerId: user1.id }); // backfill album user - await ctx.newAlbumUser({ albumId: album1.id, userId: user1.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: user1.id, role: AlbumUserRole.Editor }); await wait(2); // initial album user - await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor }); await wait(2); // post checkpoint album user - await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(1); @@ -238,7 +238,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { ack: expect.any(String), data: expect.objectContaining({ albumId: album2.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, userId: auth.user.id, }), type: SyncEntityType.AlbumUserV1, @@ -248,7 +248,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { // ack initial user await ctx.syncAckAll(auth, response); // get access to the backfill album user - await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor }); // should backfill the album user const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); @@ -257,7 +257,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { ack: expect.any(String), data: expect.objectContaining({ albumId: album1.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, userId: user1.id, }), type: SyncEntityType.AlbumUserBackfillV1, @@ -271,7 +271,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { ack: expect.any(String), data: expect.objectContaining({ albumId: album1.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, userId: user2.id, }), type: SyncEntityType.AlbumUserV1, @@ -280,7 +280,7 @@ describe(SyncRequestType.AlbumUsersV1, () => { ack: expect.any(String), data: expect.objectContaining({ albumId: album1.id, - role: AlbumUserRole.EDITOR, + role: AlbumUserRole.Editor, userId: auth.user.id, }), type: SyncEntityType.AlbumUserV1, diff --git a/server/test/medium/specs/sync/sync-album.spec.ts b/server/test/medium/specs/sync/sync-album.spec.ts index 83e9f12651..9f44e617e3 100644 --- a/server/test/medium/specs/sync/sync-album.spec.ts +++ b/server/test/medium/specs/sync/sync-album.spec.ts @@ -101,7 +101,7 @@ describe(SyncRequestType.AlbumsV1, () => { const { auth, ctx } = await setup(); const { user: user2 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: user2.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); expect(response).toHaveLength(1); @@ -121,7 +121,7 @@ describe(SyncRequestType.AlbumsV1, () => { const { auth, ctx } = await setup(); const { user: user2 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: user2.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); expect(response).toHaveLength(1); @@ -153,7 +153,7 @@ describe(SyncRequestType.AlbumsV1, () => { ]); await ctx.syncAckAll(auth, response); - await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.Editor }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); expect(newResponse).toHaveLength(1); @@ -174,7 +174,7 @@ describe(SyncRequestType.AlbumsV1, () => { const albumRepo = ctx.get(AlbumRepository); const { user: user2 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: user2.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); expect(response).toHaveLength(1); @@ -202,7 +202,7 @@ describe(SyncRequestType.AlbumsV1, () => { const albumUserRepo = ctx.get(AlbumUserRepository); const { user: user2 } = await ctx.newUser(); const { album } = await ctx.newAlbum({ ownerId: user2.id }); - await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); expect(response).toHaveLength(1); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts new file mode 100644 index 0000000000..68d3007c52 --- /dev/null +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -0,0 +1,92 @@ +import { Kysely } from 'kysely'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.AssetFaceV1, () => { + it('should detect and sync the first asset face', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetFace.id, + assetId: asset.id, + personId: person.id, + imageWidth: assetFace.imageWidth, + imageHeight: assetFace.imageHeight, + boundingBoxX1: assetFace.boundingBoxX1, + boundingBoxY1: assetFace.boundingBoxY1, + boundingBoxX2: assetFace.boundingBoxX2, + boundingBoxY2: assetFace.boundingBoxY2, + sourceType: assetFace.sourceType, + }), + type: 'AssetFaceV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + }); + + it('should detect and sync a deleted asset face', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + await personRepo.deleteAssetFace(assetFace.id); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetFaceId: assetFace.id, + }, + type: 'AssetFaceDeleteV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).resolves.toEqual([]); + }); + + it('should not sync an asset face or asset face delete for an unrelated user', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { user: user2 } = await ctx.newUser(); + const { session } = await ctx.newSession({ userId: user2.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + const auth2 = factory.auth({ session, user: user2 }); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + + await personRepo.deleteAssetFace(assetFace.id); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1])).toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-person.spec.ts b/server/test/medium/specs/sync/sync-person.spec.ts index 807e41894c..fbf401e377 100644 --- a/server/test/medium/specs/sync/sync-person.spec.ts +++ b/server/test/medium/specs/sync/sync-person.spec.ts @@ -31,7 +31,6 @@ describe(SyncEntityType.PersonV1, () => { data: expect.objectContaining({ id: person.id, name: person.name, - thumbnailPath: person.thumbnailPath, isHidden: person.isHidden, birthDate: person.birthDate, faceAssetId: person.faceAssetId, diff --git a/server/test/medium/specs/sync/sync-user-metadata.spec.ts b/server/test/medium/specs/sync/sync-user-metadata.spec.ts index bb4a500a60..7cd53e76e3 100644 --- a/server/test/medium/specs/sync/sync-user-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-user-metadata.spec.ts @@ -22,7 +22,7 @@ describe(SyncEntityType.UserMetadataV1, () => { const { auth, user, ctx } = await setup(); const userRepo = ctx.get(UserRepository); - await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); expect(response).toHaveLength(1); @@ -30,7 +30,7 @@ describe(SyncEntityType.UserMetadataV1, () => { { ack: expect.any(String), data: { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, userId: user.id, value: { isOnboarded: true }, }, @@ -46,7 +46,7 @@ describe(SyncEntityType.UserMetadataV1, () => { const { auth, user, ctx } = await setup(); const userRepo = ctx.get(UserRepository); - await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); expect(response).toHaveLength(1); @@ -54,7 +54,7 @@ describe(SyncEntityType.UserMetadataV1, () => { { ack: expect.any(String), data: { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, userId: user.id, value: { isOnboarded: true }, }, @@ -64,14 +64,14 @@ describe(SyncEntityType.UserMetadataV1, () => { await ctx.syncAckAll(auth, response); - await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: false } }); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: false } }); const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); expect(updatedResponse).toEqual([ { ack: expect.any(String), data: { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, userId: user.id, value: { isOnboarded: false }, }, @@ -89,7 +89,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { const { auth, user, ctx } = await setup(); const userRepo = ctx.get(UserRepository); - await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.Onboarding, value: { isOnboarded: true } }); const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); expect(response).toHaveLength(1); @@ -97,7 +97,7 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { { ack: expect.any(String), data: { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, userId: user.id, value: { isOnboarded: true }, }, @@ -107,14 +107,14 @@ describe(SyncEntityType.UserMetadataDeleteV1, () => { await ctx.syncAckAll(auth, response); - await userRepo.deleteMetadata(auth.user.id, UserMetadataKey.ONBOARDING); + await userRepo.deleteMetadata(auth.user.id, UserMetadataKey.Onboarding); await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([ { ack: expect.any(String), data: { userId: user.id, - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, }, type: 'UserMetadataDeleteV1', }, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index a29babbf54..6fca29d98e 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -39,5 +39,6 @@ export const newAssetRepositoryMock = (): Mocked) => ({ ...envData, ...confi export const newConfigRepositoryMock = (): Mocked> => { return { getEnv: vitest.fn().mockReturnValue(mockEnvData({})), - getWorker: vitest.fn().mockReturnValue(ImmichWorker.API), + getWorker: vitest.fn().mockReturnValue(ImmichWorker.Api), isDev: vitest.fn().mockReturnValue(false), }; }; diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index abdde53e9d..3664730be2 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -23,5 +23,6 @@ export const newDatabaseRepositoryMock = (): Mocked = {}) => { const authApiKeyFactory = (apiKey: Partial = {}) => ({ id: newUuid(), - permissions: [Permission.ALL], + permissions: [Permission.All], ...apiKey, }); @@ -154,7 +154,7 @@ const userFactory = (user: Partial = {}) => ({ profileChangedAt: newDate(), metadata: [ { - key: UserMetadataKey.ONBOARDING, + key: UserMetadataKey.Onboarding, value: 'true', }, ] as UserMetadataItem[], @@ -178,7 +178,7 @@ const userAdminFactory = (user: Partial = {}) => { oauthId = '', quotaSizeInBytes = null, quotaUsageInBytes = 0, - status = UserStatus.ACTIVE, + status = UserStatus.Active, metadata = [], } = user; return { @@ -208,7 +208,7 @@ const assetFactory = (asset: Partial = {}) => ({ updatedAt: newDate(), deletedAt: null, updateId: newUuidV7(), - status: AssetStatus.ACTIVE, + status: AssetStatus.Active, checksum: newSha1(), deviceAssetId: '', deviceId: '', @@ -229,8 +229,8 @@ const assetFactory = (asset: Partial = {}) => ({ sidecarPath: null, stackId: null, thumbhash: null, - type: AssetType.IMAGE, - visibility: AssetVisibility.TIMELINE, + type: AssetType.Image, + visibility: AssetVisibility.Timeline, ...asset, }); @@ -258,7 +258,7 @@ const apiKeyFactory = (apiKey: Partial = {}) => ({ updatedAt: newDate(), updateId: newUuidV7(), name: 'Api Key', - permissions: [Permission.ALL], + permissions: [Permission.All], ...apiKey, }); @@ -284,7 +284,7 @@ const memoryFactory = (memory: Partial = {}) => ({ updateId: newUuidV7(), deletedAt: null, ownerId: newUuid(), - type: MemoryType.ON_THIS_DAY, + type: MemoryType.OnThisDay, data: { year: 2024 } as OnThisDayData, isSaved: false, memoryAt: newDate(), diff --git a/web/Dockerfile b/web/Dockerfile index 1c6c4b46bf..3c119fdd4d 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,11 +1,17 @@ FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e -RUN apk add --no-cache tini +RUN apk add --no-cache tini bash + USER node WORKDIR /usr/src/app -COPY --chown=node:node package*.json ./ + +COPY --chown=node:node ./web/package* ./web/ + +WORKDIR /usr/src/app/web RUN npm ci -ENV CHOKIDAR_USEPOLLING=true + +ENV CHOKIDAR_USEPOLLING=true \ + PATH="${PATH}:/usr/src/app/web/bin" EXPOSE 24678 EXPOSE 3000 -ENTRYPOINT ["/sbin/tini", "--", "/bin/sh"] +ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] diff --git a/web/bin/immich-web b/web/bin/immich-web index ea748863db..d2739cf6c3 100755 --- a/web/bin/immich-web +++ b/web/bin/immich-web @@ -1,10 +1,11 @@ #!/usr/bin/env sh -TYPESCRIPT_SDK=/usr/src/open-api/typescript-sdk +TYPESCRIPT_SDK=/usr/src/app/open-api/typescript-sdk npm --prefix "$TYPESCRIPT_SDK" install npm --prefix "$TYPESCRIPT_SDK" run build +cd /usr/src/app/web || exit 1 COUNT=0 UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}" @@ -18,4 +19,4 @@ done echo "Connected to $UPSTREAM" -node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000 +npx vite dev --host 0.0.0.0 --port 3000 diff --git a/web/src/app.html b/web/src/app.html index 776764850f..ec8c4393ff 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -21,6 +21,11 @@ html { height: 100%; width: 100%; + background-color: rgb(255, 255, 255); + } + + html.dark { + background-color: rgb(10, 10, 10); } body, @@ -29,6 +34,10 @@ padding: 0; } + body { + transition: background-color 0.15s ease; + } + @keyframes delayedVisibility { to { visibility: visible; diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 04f6d94e0f..e07e5e99c6 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -343,11 +343,12 @@
{/if}