mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/search-filter-album/web
This commit is contained in:
@@ -11,8 +11,8 @@ services:
|
|||||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
- server_node_modules:/workspaces/immich/server/node_modules
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
- web_node_modules:/workspaces/immich/web/node_modules
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ services:
|
|||||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
- server_node_modules:/workspaces/immich/server/node_modules
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
- web_node_modules:/workspaces/immich/web/node_modules
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
|
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/usr/src/app/upload
|
||||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/usr/src/app/upload/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
|
|||||||
+16
-16
@@ -1,41 +1,41 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.github/
|
.github/
|
||||||
.git/
|
.git/
|
||||||
|
.env*
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
**/Dockerfile
|
||||||
|
**/node_modules/
|
||||||
|
**/.pnpm-store/
|
||||||
|
**/dist/
|
||||||
|
**/coverage/
|
||||||
|
**/build/
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
Dockerfile
|
|
||||||
!docker/scripts
|
!docker/scripts
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
!docs/package.json
|
!docs/package.json
|
||||||
!docs/package-lock.json
|
!docs/package-lock.json
|
||||||
|
|
||||||
e2e/
|
e2e/
|
||||||
!e2e/package.json
|
!e2e/package.json
|
||||||
!e2e/package-lock.json
|
!e2e/package-lock.json
|
||||||
|
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
cli/coverage/
|
|
||||||
cli/dist/
|
|
||||||
cli/node_modules/
|
|
||||||
cli/Dockerfile
|
|
||||||
|
|
||||||
open-api/typescript-sdk/build/
|
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/upload/
|
||||||
server/src/queries
|
server/src/queries
|
||||||
server/dist/
|
|
||||||
server/www/
|
server/www/
|
||||||
server/Dockerfile
|
|
||||||
|
|
||||||
web/node_modules/
|
|
||||||
web/coverage/
|
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
|
||||||
web/.env
|
|
||||||
web/Dockerfile
|
|
||||||
|
|||||||
@@ -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 -diff -merge
|
||||||
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
|
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 -diff -merge
|
||||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,7 @@ jobs:
|
|||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
|
||||||
- name: Install DCM
|
- name: Install DCM
|
||||||
# TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
|
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
||||||
uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
|
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
version: auto
|
version: auto
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ mobile/android/fastlane/report.xml
|
|||||||
mobile/ios/fastlane/report.xml
|
mobile/ios/fastlane/report.xml
|
||||||
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
|
.pnpm-store
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ name: immich-dev
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['/usr/src/app/bin/immich-dev']
|
command: ['immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -27,11 +27,11 @@ services:
|
|||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app/server
|
||||||
- ../open-api:/usr/src/open-api
|
- ../open-api:/usr/src/app/open-api
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/server/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -69,19 +69,20 @@ services:
|
|||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||||
# user: 0:0
|
# user: 0:0
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../
|
||||||
command: ['/usr/src/app/bin/immich-web']
|
dockerfile: web/Dockerfile
|
||||||
|
command: ['immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app/web
|
||||||
- ../i18n:/usr/src/i18n
|
- ../i18n:/usr/src/app/i18n
|
||||||
- ../open-api/:/usr/src/open-api/
|
- ../open-api/:/usr/src/app/open-api/
|
||||||
# - ../../ui:/usr/ui
|
# - ../../ui:/usr/ui
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/web/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
|
The `immich-server` docker image comes preinstalled with an administrative CLI (`immich-admin`) that supports the following commands:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------------ | ------------------------------------- |
|
| ------------------------ | ------------------------------------------------------------- |
|
||||||
| `help` | Display help |
|
| `help` | Display help |
|
||||||
| `reset-admin-password` | Reset the password for the admin user |
|
| `reset-admin-password` | Reset the password for the admin user |
|
||||||
| `disable-password-login` | Disable password login |
|
| `disable-password-login` | Disable password login |
|
||||||
| `enable-password-login` | Enable password login |
|
| `enable-password-login` | Enable password login |
|
||||||
| `enable-oauth-login` | Enable OAuth login |
|
| `enable-oauth-login` | Enable OAuth login |
|
||||||
| `disable-oauth-login` | Disable OAuth login |
|
| `disable-oauth-login` | Disable OAuth login |
|
||||||
| `list-users` | List Immich users |
|
| `list-users` | List Immich users |
|
||||||
| `version` | Print Immich version |
|
| `version` | Print Immich version |
|
||||||
|
| `change-media-location` | Change database file paths to align with a new media location |
|
||||||
|
|
||||||
## How to run a command
|
## How to run a command
|
||||||
|
|
||||||
@@ -88,3 +89,24 @@ Print Immich Version
|
|||||||
immich-admin version
|
immich-admin version
|
||||||
v1.129.0
|
v1.129.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Change media location
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin change-media-location
|
||||||
|
? Enter the previous value of IMMICH_MEDIA_LOCATION: /usr/src/app/upload
|
||||||
|
? Enter the new value of IMMICH_MEDIA_LOCATION: /data
|
||||||
|
|
||||||
|
Previous value: /usr/src/app/upload
|
||||||
|
Current value: /data
|
||||||
|
|
||||||
|
Changing database paths from "/usr/src/app/upload/*" to "/data/*"
|
||||||
|
|
||||||
|
? Do you want to proceed? [Y/n] y
|
||||||
|
|
||||||
|
Database file paths updated successfully! 🎉
|
||||||
|
|
||||||
|
You may now set IMMICH_MEDIA_LOCATION=/data and restart!
|
||||||
|
|
||||||
|
(please remember to update applicable volume mounts e.g. ${UPLOAD_LOCATION}:/data)
|
||||||
|
```
|
||||||
|
|||||||
@@ -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.
|
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
|
||||||
|
|
||||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
|
Get started fast!
|
||||||
|
|
||||||
[](https://codespaces.new/immich-app/immich/)
|
[](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.
|
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.
|
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
|
```bash
|
||||||
@@ -88,6 +88,10 @@ source ~/.bashrc
|
|||||||
|
|
||||||
### Step 3: Launch the Dev Container
|
### 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:
|
#### Using VS Code UI:
|
||||||
|
|
||||||
1. Open the cloned repository in VS Code
|
1. Open the cloned repository in VS Code
|
||||||
|
|||||||
@@ -29,20 +29,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
| Variable | Description | Default | Containers | Workers |
|
| Variable | Description | Default | Containers | Workers |
|
||||||
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :---------------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/usr/src/app/upload`<sup>\*3</sup> | server | api, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
||||||
|
|
||||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||||
|
|||||||
Vendored
+3
-2
@@ -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/mobile-app-beta-program /docs/features/mobile-app 307
|
||||||
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
|
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
|
||||||
/docs/install /docs/install/docker-compose 307
|
/docs/install /docs/install/docker-compose 307
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
/docs/guides/api-album-sync /docs/community-projects 307
|
/docs/guides/api-album-sync /docs/community-projects 307
|
||||||
/docs/guides/remove-offline-files /docs/community-projects 307
|
/docs/guides/remove-offline-files /docs/community-projects 307
|
||||||
/milestones /roadmap 307
|
/milestones /roadmap 307
|
||||||
/docs/overview/introduction /docs/overview/welcome 307
|
/docs/overview/introduction /docs/overview/welcome 307
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ name: immich-e2e
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
container_name: immich-e2e-server
|
||||||
command: ['./start.sh']
|
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
|
|||||||
+18
-1
@@ -373,10 +373,12 @@
|
|||||||
"admin_password": "Admin Password",
|
"admin_password": "Admin Password",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
|
"advanced_settings_beta_timeline_subtitle": "Try the new app experience",
|
||||||
|
"advanced_settings_beta_timeline_title": "Beta Timeline",
|
||||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
||||||
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
||||||
"advanced_settings_log_level_title": "Log level: {level}",
|
"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_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_subtitle": "Define proxy headers Immich should send with each network request",
|
||||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||||
@@ -404,6 +406,7 @@
|
|||||||
"album_options": "Album options",
|
"album_options": "Album options",
|
||||||
"album_remove_user": "Remove user?",
|
"album_remove_user": "Remove user?",
|
||||||
"album_remove_user_confirmation": "Are you sure you want to 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_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": "Album updated",
|
||||||
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
|
"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": "Default album sort order",
|
||||||
"albums_default_sort_order_description": "Initial asset sort order when creating new albums.",
|
"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_feature_description": "Collections of assets that can be shared with other users.",
|
||||||
|
"albums_on_device_count": "Albums on device ({count})",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"all_albums": "All albums",
|
"all_albums": "All albums",
|
||||||
"all_people": "All people",
|
"all_people": "All people",
|
||||||
@@ -603,6 +607,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cancel_search": "Cancel search",
|
"cancel_search": "Cancel search",
|
||||||
"canceled": "Canceled",
|
"canceled": "Canceled",
|
||||||
|
"canceling": "Canceling",
|
||||||
"cannot_merge_people": "Cannot merge people",
|
"cannot_merge_people": "Cannot merge people",
|
||||||
"cannot_undo_this_action": "You cannot undo this action!",
|
"cannot_undo_this_action": "You cannot undo this action!",
|
||||||
"cannot_update_the_description": "Cannot update the description",
|
"cannot_update_the_description": "Cannot update the description",
|
||||||
@@ -749,6 +754,7 @@
|
|||||||
"delete_key": "Delete key",
|
"delete_key": "Delete key",
|
||||||
"delete_library": "Delete Library",
|
"delete_library": "Delete Library",
|
||||||
"delete_link": "Delete link",
|
"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_backed_up_only": "Delete Backed Up Only",
|
||||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||||
"delete_others": "Delete others",
|
"delete_others": "Delete others",
|
||||||
@@ -762,6 +768,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"deselect_all": "Deselect All",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"direction": "Direction",
|
"direction": "Direction",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@@ -779,6 +786,7 @@
|
|||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"download_action_prompt": "Downloading {count} assets",
|
||||||
"download_canceled": "Download canceled",
|
"download_canceled": "Download canceled",
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
"download_enqueue": "Download enqueued",
|
"download_enqueue": "Download enqueued",
|
||||||
@@ -835,6 +843,7 @@
|
|||||||
"empty_trash": "Empty trash",
|
"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!",
|
"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": "Enable",
|
||||||
|
"enable_backup": "Enable Backup",
|
||||||
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
@@ -1146,6 +1155,7 @@
|
|||||||
"library_page_sort_created": "Created date",
|
"library_page_sort_created": "Created date",
|
||||||
"library_page_sort_last_modified": "Last modified",
|
"library_page_sort_last_modified": "Last modified",
|
||||||
"library_page_sort_title": "Album title",
|
"library_page_sort_title": "Album title",
|
||||||
|
"licenses": "Licenses",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
"link_motion_video": "Link motion video",
|
"link_motion_video": "Link motion video",
|
||||||
@@ -1480,6 +1490,7 @@
|
|||||||
"purchase_server_description_2": "Supporter status",
|
"purchase_server_description_2": "Supporter status",
|
||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
|
"queue_status": "Queuing {count}/{total}",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||||
@@ -1690,6 +1701,7 @@
|
|||||||
"settings_saved": "Settings saved",
|
"settings_saved": "Settings saved",
|
||||||
"setup_pin_code": "Setup a PIN code",
|
"setup_pin_code": "Setup a PIN code",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
|
"share_action_prompt": "Shared {count} assets",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_assets_selected": "{count} selected",
|
"share_assets_selected": "{count} selected",
|
||||||
"share_dialog_preparing": "Preparing...",
|
"share_dialog_preparing": "Preparing...",
|
||||||
@@ -1791,6 +1803,7 @@
|
|||||||
"sort_title": "Title",
|
"sort_title": "Title",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"stack": "Stack",
|
"stack": "Stack",
|
||||||
|
"stack_action_prompt": "{count} stacked",
|
||||||
"stack_duplicates": "Stack duplicates",
|
"stack_duplicates": "Stack duplicates",
|
||||||
"stack_select_one_photo": "Select one main photo for the stack",
|
"stack_select_one_photo": "Select one main photo for the stack",
|
||||||
"stack_selected_photos": "Stack selected photos",
|
"stack_selected_photos": "Stack selected photos",
|
||||||
@@ -1901,6 +1914,7 @@
|
|||||||
"unselect_all_duplicates": "Unselect all duplicates",
|
"unselect_all_duplicates": "Unselect all duplicates",
|
||||||
"unselect_all_in": "Unselect all in {group}",
|
"unselect_all_in": "Unselect all in {group}",
|
||||||
"unstack": "Un-stack",
|
"unstack": "Un-stack",
|
||||||
|
"unstack_action_prompt": "{count} unstacked",
|
||||||
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
|
||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
@@ -1908,6 +1922,7 @@
|
|||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"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_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_title": "Upload Asset",
|
"upload_dialog_title": "Upload Asset",
|
||||||
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
|
"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",
|
"user_usage_stats_description": "View account usage statistics",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
"users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
|
||||||
"utilities": "Utilities",
|
"utilities": "Utilities",
|
||||||
"validate": "Validate",
|
"validate": "Validate",
|
||||||
"validate_endpoint_error": "Please enter a valid URL",
|
"validate_endpoint_error": "Please enter a valid URL",
|
||||||
@@ -1957,6 +1973,7 @@
|
|||||||
"view_album": "View Album",
|
"view_album": "View Album",
|
||||||
"view_all": "View All",
|
"view_all": "View All",
|
||||||
"view_all_users": "View all users",
|
"view_all_users": "View all users",
|
||||||
|
"view_details": "View Details",
|
||||||
"view_in_timeline": "View in timeline",
|
"view_in_timeline": "View in timeline",
|
||||||
"view_link": "View link",
|
"view_link": "View link",
|
||||||
"view_links": "View links",
|
"view_links": "View links",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ custom_lint:
|
|||||||
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||||
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
|
||||||
|
- lib/domain/services/search.service.dart
|
||||||
|
|
||||||
# refactor
|
# refactor
|
||||||
- lib/models/map/map_marker.model.dart
|
- lib/models/map/map_marker.model.dart
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ plugins {
|
|||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
@@ -45,6 +47,10 @@ android {
|
|||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 26
|
minSdkVersion 26
|
||||||
@@ -105,6 +111,8 @@ dependencies {
|
|||||||
def guava_version = '33.3.1-android'
|
def guava_version = '33.3.1-android'
|
||||||
def glide_version = '4.16.0'
|
def glide_version = '4.16.0'
|
||||||
def serialization_version = '1.8.1'
|
def serialization_version = '1.8.1'
|
||||||
|
def compose_version = '1.1.1'
|
||||||
|
def gson_version = '2.10.1'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
@@ -116,6 +124,17 @@ dependencies {
|
|||||||
|
|
||||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
||||||
|
|
||||||
|
//Glance Widget
|
||||||
|
implementation "androidx.glance:glance-appwidget:$compose_version"
|
||||||
|
implementation "com.google.code.gson:gson:$gson_version"
|
||||||
|
|
||||||
|
// Glance Configure
|
||||||
|
implementation "androidx.activity:activity-compose:1.8.2"
|
||||||
|
implementation "androidx.compose.ui:ui:$compose_version"
|
||||||
|
implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||||
|
implementation "androidx.compose.material3:material3:1.2.1"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is uncommented in F-Droid build script
|
// This is uncommented in F-Droid build script
|
||||||
|
|||||||
+8
-1
@@ -25,8 +25,15 @@
|
|||||||
@com.google.gson.annotations.SerializedName <fields>;
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TypeToken preventions
|
||||||
|
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||||
|
-keep class * extends com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
##---------------End: proguard configuration for Gson ----------
|
##---------------End: proguard configuration for Gson ----------
|
||||||
|
|
||||||
|
# Keep all widget model classes and their fields for Gson
|
||||||
|
-keep class app.alextran.immich.widget.model.** { *; }
|
||||||
@@ -141,6 +141,41 @@
|
|||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Widgets -->
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.RandomReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/random_widget_title">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/random_widget" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.MemoryReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/memory_widget_title">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/memory_widget" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<activity android:name=".widget.configure.RandomConfigure"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
@@ -154,4 +189,4 @@
|
|||||||
<data android:scheme="geo" />
|
<data android:scheme="geo" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
|
|
||||||
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
runDart()
|
runDart()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvableFuture
|
return resolvableFuture
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? {
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||||
|
|
||||||
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
|
||||||
|
return BitmapFactory.decodeFile(file.absolutePath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||||
|
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||||
|
var inSampleSize = 1
|
||||||
|
|
||||||
|
if (height > reqHeight || width > reqWidth) {
|
||||||
|
val halfHeight: Int = height / 2
|
||||||
|
val halfWidth: Int = width / 2
|
||||||
|
|
||||||
|
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||||
|
inSampleSize *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inSampleSize
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.glance.*
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.work.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class ImageDownloadWorker(
|
||||||
|
private val context: Context,
|
||||||
|
workerParameters: WorkerParameters
|
||||||
|
) : CoroutineWorker(context, workerParameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
||||||
|
|
||||||
|
private fun buildConstraints(): Constraints {
|
||||||
|
return Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putString(kWorkerWidgetType, widgetType.toString())
|
||||||
|
.putInt(kWorkerWidgetID, appWidgetId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
||||||
|
20, TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
.setConstraints(buildConstraints())
|
||||||
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
|
.addTag(appWidgetId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.enqueueUniquePeriodicWork(
|
||||||
|
"$uniqueWorkName-$appWidgetId",
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
|
||||||
|
.setConstraints(buildConstraints())
|
||||||
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
|
.addTag(appWidgetId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.enqueueUniqueWork(
|
||||||
|
"$uniqueWorkName-$appWidgetId",
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancel(context: Context, appWidgetId: Int) {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId")
|
||||||
|
|
||||||
|
// delete cached image
|
||||||
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
||||||
|
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentImgUUID = widgetConfig[kImageUUID]
|
||||||
|
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(currentImgUUID))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||||
|
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||||
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||||
|
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentImgUUID = widgetConfig[kImageUUID]
|
||||||
|
|
||||||
|
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||||
|
|
||||||
|
// clear any image caches and go to "login" state if no credentials
|
||||||
|
if (serverConfig == null) {
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
deleteImage(currentImgUUID)
|
||||||
|
updateWidget(
|
||||||
|
glanceId,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"immich://",
|
||||||
|
WidgetState.LOG_IN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch new image
|
||||||
|
val entry = when (widgetType) {
|
||||||
|
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
|
||||||
|
WidgetType.MEMORIES -> fetchMemory(serverConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear current image if it exists
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
deleteImage(currentImgUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save a new image
|
||||||
|
val imgUUID = UUID.randomUUID().toString()
|
||||||
|
saveImage(entry.image, imgUUID)
|
||||||
|
|
||||||
|
// trigger the update routine with new image uuid
|
||||||
|
updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(uniqueWorkName, "Error while loading image", e)
|
||||||
|
if (runAttemptCount < 10) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateWidget(
|
||||||
|
glanceId: GlanceId,
|
||||||
|
imageUUID: String,
|
||||||
|
subtitle: String?,
|
||||||
|
deeplink: String?,
|
||||||
|
widgetState: WidgetState = WidgetState.SUCCESS
|
||||||
|
) {
|
||||||
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
|
prefs[kNow] = System.currentTimeMillis()
|
||||||
|
prefs[kImageUUID] = imageUUID
|
||||||
|
prefs[kWidgetState] = widgetState.toString()
|
||||||
|
prefs[kSubtitleText] = subtitle ?: ""
|
||||||
|
prefs[kDeeplinkURL] = deeplink ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoWidget().update(context,glanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchRandom(
|
||||||
|
serverConfig: ServerConfig,
|
||||||
|
widgetConfig: Preferences
|
||||||
|
): WidgetEntry {
|
||||||
|
val api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
|
val filters = SearchFilters()
|
||||||
|
val albumId = widgetConfig[kSelectedAlbum]
|
||||||
|
val showSubtitle = widgetConfig[kShowAlbumName]
|
||||||
|
val albumName = widgetConfig[kSelectedAlbumName]
|
||||||
|
var subtitle: String? = if (showSubtitle == true) albumName else ""
|
||||||
|
|
||||||
|
|
||||||
|
if (albumId == "FAVORITES") {
|
||||||
|
filters.isFavorite = true
|
||||||
|
} else if (albumId != null) {
|
||||||
|
filters.albumIds = listOf(albumId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var randomSearch = api.fetchSearchResults(filters)
|
||||||
|
|
||||||
|
// handle an empty album, fallback to random
|
||||||
|
if (randomSearch.isEmpty() && albumId != null) {
|
||||||
|
randomSearch = api.fetchSearchResults(SearchFilters())
|
||||||
|
subtitle = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val random = randomSearch.first()
|
||||||
|
val image = api.fetchImage(random)
|
||||||
|
|
||||||
|
return WidgetEntry(
|
||||||
|
image,
|
||||||
|
subtitle,
|
||||||
|
assetDeeplink(random)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchMemory(
|
||||||
|
serverConfig: ServerConfig
|
||||||
|
): WidgetEntry {
|
||||||
|
val api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val memories = api.fetchMemory(today)
|
||||||
|
val asset: Asset
|
||||||
|
var subtitle: String? = null
|
||||||
|
|
||||||
|
if (memories.isNotEmpty()) {
|
||||||
|
// pick a random asset from a random memory
|
||||||
|
val memory = memories.random()
|
||||||
|
asset = memory.assets.random()
|
||||||
|
|
||||||
|
val yearDiff = today.year - memory.data.year
|
||||||
|
subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago"
|
||||||
|
} else {
|
||||||
|
val filters = SearchFilters(size=1)
|
||||||
|
asset = api.fetchSearchResults(filters).first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val image = api.fetchImage(asset)
|
||||||
|
return WidgetEntry(
|
||||||
|
image,
|
||||||
|
subtitle,
|
||||||
|
assetDeeplink(asset)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
|
FileOutputStream(file).use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class ImmichAPI(cfg: ServerConfig) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getServerConfig(context: Context): ServerConfig? {
|
||||||
|
val prefs = HomeWidgetPlugin.getData(context)
|
||||||
|
|
||||||
|
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
||||||
|
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
||||||
|
|
||||||
|
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerConfig(
|
||||||
|
serverURL,
|
||||||
|
sessionKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
private val serverConfig = cfg
|
||||||
|
|
||||||
|
private fun buildRequestURL(endpoint: String, params: List<Pair<String, String>> = emptyList()): URL {
|
||||||
|
val urlString = StringBuilder("${serverConfig.serverEndpoint}$endpoint?sessionKey=${serverConfig.sessionKey}")
|
||||||
|
|
||||||
|
for ((key, value) in params) {
|
||||||
|
urlString.append("&${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL(urlString.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
||||||
|
val url = buildRequestURL("/search/random")
|
||||||
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "POST"
|
||||||
|
setRequestProperty("Content-Type", "application/json")
|
||||||
|
doOutput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.outputStream.use {
|
||||||
|
OutputStreamWriter(it).use { writer ->
|
||||||
|
writer.write(gson.toJson(filters))
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
|
val type = object : TypeToken<List<Asset>>() {}.type
|
||||||
|
gson.fromJson(response, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchMemory(date: LocalDate): List<MemoryResult> = withContext(Dispatchers.IO) {
|
||||||
|
val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
|
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
||||||
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
|
val type = object : TypeToken<List<MemoryResult>>() {}.type
|
||||||
|
gson.fromJson(response, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) {
|
||||||
|
val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview"))
|
||||||
|
val connection = url.openConnection()
|
||||||
|
val data = connection.getInputStream().readBytes()
|
||||||
|
BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
|
?: throw Exception("Invalid image data")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchAlbums(): List<Album> = withContext(Dispatchers.IO) {
|
||||||
|
val url = buildRequestURL("/albums")
|
||||||
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
|
val type = object : TypeToken<List<Album>>() {}.type
|
||||||
|
gson.fromJson(response, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MemoryReceiver : GlanceAppWidgetReceiver() {
|
||||||
|
override val glanceAppWidget = PhotoWidget()
|
||||||
|
|
||||||
|
override fun onUpdate(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetIds: IntArray
|
||||||
|
) {
|
||||||
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
|
||||||
|
appWidgetIds.forEach { widgetID ->
|
||||||
|
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
||||||
|
|
||||||
|
// Launch coroutine to setup a single shot if the app requested the update
|
||||||
|
if (fromMainApp) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val provider = ComponentName(context, MemoryReceiver::class.java)
|
||||||
|
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
||||||
|
|
||||||
|
glanceIds.forEach { widgetID ->
|
||||||
|
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.MEMORIES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||||
|
super.onDeleted(context, appWidgetIds)
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
appWidgetIds.forEach { id ->
|
||||||
|
ImageDownloadWorker.cancel(context, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.datastore.preferences.core.MutablePreferences
|
||||||
|
import androidx.glance.appwidget.*
|
||||||
|
import androidx.glance.*
|
||||||
|
import androidx.glance.action.clickable
|
||||||
|
import androidx.glance.layout.*
|
||||||
|
import androidx.glance.state.GlanceStateDefinition
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import androidx.glance.unit.ColorProvider
|
||||||
|
import app.alextran.immich.R
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class PhotoWidget : GlanceAppWidget() {
|
||||||
|
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
||||||
|
|
||||||
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||||
|
provideContent {
|
||||||
|
val prefs = currentState<MutablePreferences>()
|
||||||
|
|
||||||
|
val imageUUID = prefs[kImageUUID]
|
||||||
|
val subtitle = prefs[kSubtitleText]
|
||||||
|
val deeplinkURL = prefs[kDeeplinkURL]?.toUri()
|
||||||
|
val widgetState = prefs[kWidgetState]
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
|
||||||
|
if (imageUUID != null) {
|
||||||
|
// fetch a random photo from server
|
||||||
|
val file = File(context.cacheDir, imageFilename(imageUUID))
|
||||||
|
|
||||||
|
if (file.exists()) {
|
||||||
|
bitmap = loadScaledBitmap(file, 500, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIDGET CONTENT
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(GlanceTheme.colors.background)
|
||||||
|
.clickable {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, deeplinkURL ?: "immich://".toUri())
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (bitmap != null) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(bitmap),
|
||||||
|
contentDescription = "Widget Image",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = GlanceModifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!subtitle.isNullOrBlank()) {
|
||||||
|
Column(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = TextStyle(
|
||||||
|
color = ColorProvider(Color.White),
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.background(ColorProvider(Color(0x99000000))) // 60% black
|
||||||
|
.padding(8.dp)
|
||||||
|
.cornerRadius(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(R.drawable.splash),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (widgetState == WidgetState.LOG_IN.toString()) {
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("Log in to your Immich server", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = GlanceModifier.fillMaxWidth().padding(16.dp)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = GlanceModifier.size(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||||
|
|
||||||
|
Text("Loading widget...", style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.primary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import es.antonborri.home_widget.HomeWidgetPlugin
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RandomReceiver : GlanceAppWidgetReceiver() {
|
||||||
|
override val glanceAppWidget = PhotoWidget()
|
||||||
|
|
||||||
|
override fun onUpdate(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetIds: IntArray
|
||||||
|
) {
|
||||||
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
|
||||||
|
appWidgetIds.forEach { widgetID ->
|
||||||
|
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val fromMainApp = intent.getBooleanExtra(HomeWidgetPlugin.TRIGGERED_FROM_HOME_WIDGET, false)
|
||||||
|
|
||||||
|
// Launch coroutine to setup a single shot if the app requested the update
|
||||||
|
if (fromMainApp) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val provider = ComponentName(context, RandomReceiver::class.java)
|
||||||
|
val glanceIds = AppWidgetManager.getInstance(context).getAppWidgetIds(provider)
|
||||||
|
|
||||||
|
glanceIds.forEach { widgetID ->
|
||||||
|
ImageDownloadWorker.singleShot(context, widgetID, WidgetType.RANDOM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||||
|
super.onDeleted(context, appWidgetIds)
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
appWidgetIds.forEach { id ->
|
||||||
|
ImageDownloadWorker.cancel(context, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.*
|
||||||
|
|
||||||
|
|
||||||
|
data class DropdownItem (
|
||||||
|
val label: String,
|
||||||
|
val id: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Creating a composable to display a drop down menu
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Dropdown(items: List<DropdownItem>,
|
||||||
|
selectedItem: DropdownItem?,
|
||||||
|
onItemSelected: (DropdownItem) -> Unit,
|
||||||
|
enabled: Boolean = true
|
||||||
|
) {
|
||||||
|
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var selectedOption by remember { mutableStateOf(selectedItem?.label ?: items[0].label) }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded && enabled },
|
||||||
|
) {
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = selectedOption,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
enabled = enabled,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||||
|
},
|
||||||
|
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
items.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(option.label, color = MaterialTheme.colorScheme.onSurface) },
|
||||||
|
onClick = {
|
||||||
|
selectedOption = option.label
|
||||||
|
onItemSelected(option)
|
||||||
|
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LightDarkTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val colorScheme = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme ->
|
||||||
|
dynamicDarkColorScheme(context)
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme ->
|
||||||
|
dynamicLightColorScheme(context)
|
||||||
|
isDarkTheme -> darkColorScheme()
|
||||||
|
else -> lightColorScheme()
|
||||||
|
}
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import app.alextran.immich.widget.ImageDownloadWorker
|
||||||
|
import app.alextran.immich.widget.ImmichAPI
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
class RandomConfigure : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Get widget ID from intent
|
||||||
|
val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||||
|
?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
|
||||||
|
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val glanceId = GlanceAppWidgetManager(applicationContext)
|
||||||
|
.getGlanceIdBy(appWidgetId)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
LightDarkTheme {
|
||||||
|
RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = {
|
||||||
|
finish()
|
||||||
|
Log.w("WIDGET_ACTIVITY", "SAVING")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) {
|
||||||
|
|
||||||
|
var selectedAlbum by remember { mutableStateOf<DropdownItem?>(null) }
|
||||||
|
var showAlbumName by remember { mutableStateOf(false) }
|
||||||
|
var availableAlbums by remember { mutableStateOf<List<DropdownItem>>(listOf()) }
|
||||||
|
var state by remember { mutableStateOf(WidgetConfigState.LOADING) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// get albums from server
|
||||||
|
val serverCfg = ImmichAPI.getServerConfig(context)
|
||||||
|
|
||||||
|
if (serverCfg == null) {
|
||||||
|
state = WidgetConfigState.LOG_IN
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val api = ImmichAPI(serverCfg)
|
||||||
|
|
||||||
|
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentAlbumId = currentState[kSelectedAlbum] ?: "NONE"
|
||||||
|
val currentAlbumName = currentState[kSelectedAlbumName] ?: "None"
|
||||||
|
var albumItems: List<DropdownItem>
|
||||||
|
|
||||||
|
try {
|
||||||
|
albumItems = api.fetchAlbums().map {
|
||||||
|
DropdownItem(it.albumName, it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
state = WidgetConfigState.SUCCESS
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.e("WidgetWorker", "Error fetching albums: ${e.message}")
|
||||||
|
|
||||||
|
state = WidgetConfigState.NO_CONNECTION
|
||||||
|
albumItems = listOf(DropdownItem(currentAlbumName, currentAlbumId))
|
||||||
|
}
|
||||||
|
|
||||||
|
availableAlbums = listOf(DropdownItem("None", "NONE"), DropdownItem("Favorites", "FAVORITES")) + albumItems
|
||||||
|
|
||||||
|
// load selected configuration
|
||||||
|
val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId }
|
||||||
|
selectedAlbum = albumEntity ?: availableAlbums.first()
|
||||||
|
|
||||||
|
// load showAlbumName
|
||||||
|
showAlbumName = currentState[kShowAlbumName] == true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveConfiguration() {
|
||||||
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
|
prefs[kSelectedAlbum] = selectedAlbum?.id ?: ""
|
||||||
|
prefs[kSelectedAlbumName] = selectedAlbum?.label ?: ""
|
||||||
|
prefs[kShowAlbumName] = showAlbumName
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar (
|
||||||
|
title = { Text("Widget Configuration") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
saveConfiguration()
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = "Close", tint = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding), // Respect the top bar
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||||
|
when (state) {
|
||||||
|
WidgetConfigState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||||
|
WidgetConfigState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.")
|
||||||
|
else -> {
|
||||||
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
|
||||||
|
// no connection warning
|
||||||
|
if (state == WidgetConfigState.NO_CONNECTION) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Warning,
|
||||||
|
contentDescription = "Warning",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "No connection to the server is available. Please try again later.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Album")
|
||||||
|
Dropdown(
|
||||||
|
items = availableAlbums,
|
||||||
|
selectedItem = selectedAlbum,
|
||||||
|
onItemSelected = { selectedAlbum = it },
|
||||||
|
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = "Show Album Name")
|
||||||
|
Switch(
|
||||||
|
checked = showAlbumName,
|
||||||
|
onCheckedChange = { showAlbumName = it },
|
||||||
|
enabled = (state != WidgetConfigState.NO_CONNECTION)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package app.alextran.immich.widget.model
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.datastore.preferences.core.*
|
||||||
|
|
||||||
|
// MARK: Immich Entities
|
||||||
|
|
||||||
|
enum class AssetType {
|
||||||
|
IMAGE, VIDEO, AUDIO, OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Asset(
|
||||||
|
val id: String,
|
||||||
|
val type: AssetType,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SearchFilters(
|
||||||
|
var type: AssetType = AssetType.IMAGE,
|
||||||
|
val size: Int = 1,
|
||||||
|
var albumIds: List<String> = listOf(),
|
||||||
|
var isFavorite: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MemoryResult(
|
||||||
|
val id: String,
|
||||||
|
var assets: List<Asset>,
|
||||||
|
val type: String,
|
||||||
|
val data: MemoryData
|
||||||
|
) {
|
||||||
|
data class MemoryData(val year: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Album(
|
||||||
|
val id: String,
|
||||||
|
val albumName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
// MARK: Widget Specific
|
||||||
|
|
||||||
|
enum class WidgetType {
|
||||||
|
RANDOM, MEMORIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WidgetState {
|
||||||
|
LOADING, SUCCESS, LOG_IN;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WidgetConfigState {
|
||||||
|
LOADING, SUCCESS, LOG_IN, NO_CONNECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
data class WidgetEntry (
|
||||||
|
val image: Bitmap,
|
||||||
|
val subtitle: String?,
|
||||||
|
val deeplink: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
||||||
|
|
||||||
|
// MARK: Widget State Keys
|
||||||
|
val kImageUUID = stringPreferencesKey("uuid")
|
||||||
|
val kSubtitleText = stringPreferencesKey("subtitle")
|
||||||
|
val kNow = longPreferencesKey("now")
|
||||||
|
val kWidgetState = stringPreferencesKey("state")
|
||||||
|
val kSelectedAlbum = stringPreferencesKey("albumID")
|
||||||
|
val kSelectedAlbumName = stringPreferencesKey("albumName")
|
||||||
|
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
||||||
|
val kDeeplinkURL = stringPreferencesKey("deeplink")
|
||||||
|
|
||||||
|
const val kWorkerWidgetType = "widgetType"
|
||||||
|
const val kWorkerWidgetID = "widgetId"
|
||||||
|
const val kTriggeredFromApp = "triggeredFromApp"
|
||||||
|
|
||||||
|
fun imageFilename(id: String): String {
|
||||||
|
return "widget_image_$id.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assetDeeplink(asset: Asset): String {
|
||||||
|
return "immich://asset?id=${asset.id}"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="memory_widget_title">Memories</string>
|
||||||
|
<string name="random_widget_title">Random</string>
|
||||||
|
|
||||||
|
<string name="memory_widget_description">See memories from Immich.</string>
|
||||||
|
<string name="random_widget_description">View a random image from your library or a specific album.</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="1200000"
|
||||||
|
android:description="@string/memory_widget_description"
|
||||||
|
android:previewImage="@drawable/memory_preview"
|
||||||
|
/>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="1200000"
|
||||||
|
android:configure="app.alextran.immich.widget.configure.RandomConfigure"
|
||||||
|
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||||
|
tools:targetApi="28"
|
||||||
|
android:description="@string/random_widget_description"
|
||||||
|
android:previewImage="@drawable/random_preview"
|
||||||
|
/>
|
||||||
@@ -1 +1 @@
|
|||||||
version: '>=1.29.0 <1.30.0'
|
version: '>=1.29.0 <=1.30.0'
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ class ImmichLinter extends PluginBase {
|
|||||||
return rules;
|
return rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeCode(String name, LintOptions options) => LintCode(
|
static LintCode makeCode(String name, LintOptions options) => LintCode(
|
||||||
name: name,
|
name: name,
|
||||||
problemMessage: options.json["message"] as String,
|
problemMessage: options.json["message"] as String,
|
||||||
errorSeverity: ErrorSeverity.WARNING,
|
errorSeverity: ErrorSeverity.WARNING,
|
||||||
|
|||||||
@@ -14,19 +14,6 @@ struct ImageEntry: TimelineEntry {
|
|||||||
var deepLink: URL? = nil
|
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(
|
static func build(
|
||||||
api: ImmichAPI,
|
api: ImmichAPI,
|
||||||
asset: Asset,
|
asset: Asset,
|
||||||
@@ -83,7 +70,10 @@ struct ImageEntry: TimelineEntry {
|
|||||||
|
|
||||||
guard let imageData = try? Data(contentsOf: imageURL),
|
guard let imageData = try? Data(contentsOf: imageURL),
|
||||||
let metadataJSON = try? Data(contentsOf: metadataURL),
|
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 {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -98,7 +88,7 @@ struct ImageEntry: TimelineEntry {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleCacheFallback(
|
static func handleError(
|
||||||
for key: String,
|
for key: String,
|
||||||
error: WidgetError = .fetchFailed
|
error: WidgetError = .fetchFailed
|
||||||
) -> Timeline<ImageEntry> {
|
) -> Timeline<ImageEntry> {
|
||||||
@@ -108,35 +98,32 @@ struct ImageEntry: TimelineEntry {
|
|||||||
metadata: EntryMetadata(error: error)
|
metadata: EntryMetadata(error: error)
|
||||||
)
|
)
|
||||||
|
|
||||||
// skip cache if album not found or no login
|
// use cache if generic failed error
|
||||||
// we want to show these errors to the user since without intervention,
|
// we want to show the other errors to the user since without intervention,
|
||||||
// it will never succeed
|
// it will never succeed
|
||||||
if error != .noLogin && error != .albumNotFound {
|
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
|
||||||
if let cachedEntry = ImageEntry.loadCached(for: key) {
|
{
|
||||||
timelineEntry = cachedEntry
|
timelineEntry = cachedEntry
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomEntries(
|
func generateRandomEntries(
|
||||||
api: ImmichAPI,
|
api: ImmichAPI,
|
||||||
now: Date,
|
now: Date,
|
||||||
count: Int,
|
count: Int,
|
||||||
albumId: String? = nil,
|
filter: SearchFilter = Album.NONE.filter,
|
||||||
subtitle: String? = nil
|
subtitle: String? = nil
|
||||||
)
|
)
|
||||||
async throws -> [ImageEntry]
|
async throws -> [ImageEntry]
|
||||||
{
|
{
|
||||||
|
|
||||||
var entries: [ImageEntry] = []
|
var entries: [ImageEntry] = []
|
||||||
let albumIds = albumId != nil ? [albumId!] : []
|
|
||||||
|
|
||||||
let randomAssets = try await api.fetchSearchResults(
|
let randomAssets = try await api.fetchSearchResults(with: filter)
|
||||||
with: SearchFilters(size: count, albumIds: albumIds)
|
|
||||||
)
|
|
||||||
|
|
||||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
extension Image {
|
||||||
|
@ViewBuilder
|
||||||
|
func tintedWidgetImageModifier() -> some View {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
self
|
||||||
|
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ImmichWidgetView: View {
|
struct ImmichWidgetView: View {
|
||||||
var entry: ImageEntry
|
var entry: ImageEntry
|
||||||
|
|
||||||
@@ -8,6 +20,7 @@ struct ImmichWidgetView: View {
|
|||||||
if entry.image == nil {
|
if entry.image == nil {
|
||||||
VStack {
|
VStack {
|
||||||
Image("LaunchImage")
|
Image("LaunchImage")
|
||||||
|
.tintedWidgetImageModifier()
|
||||||
Text(entry.metadata.error?.errorDescription ?? "")
|
Text(entry.metadata.error?.errorDescription ?? "")
|
||||||
.minimumScaleFactor(0.25)
|
.minimumScaleFactor(0.25)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -19,7 +32,9 @@ struct ImmichWidgetView: View {
|
|||||||
Color.clear.overlay(
|
Color.clear.overlay(
|
||||||
Image(uiImage: entry.image!)
|
Image(uiImage: entry.image!)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.tintedWidgetImageModifier()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
|
||||||
)
|
)
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||||
|
|
||||||
enum WidgetError: Error, Codable {
|
enum WidgetError: Error, Codable {
|
||||||
case noLogin
|
case noLogin
|
||||||
case fetchFailed
|
case fetchFailed
|
||||||
case unknown
|
|
||||||
case albumNotFound
|
case albumNotFound
|
||||||
|
case noAssetsAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FetchError: Error {
|
||||||
case unableToResize
|
case unableToResize
|
||||||
case invalidImage
|
case invalidImage
|
||||||
case invalidURL
|
case invalidURL
|
||||||
|
case fetchFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WidgetError: LocalizedError {
|
extension WidgetError: LocalizedError {
|
||||||
@@ -24,14 +30,8 @@ extension WidgetError: LocalizedError {
|
|||||||
case .albumNotFound:
|
case .albumNotFound:
|
||||||
return "Album not found"
|
return "Album not found"
|
||||||
|
|
||||||
case .invalidURL:
|
case .noAssetsAvailable:
|
||||||
return "An invalid URL was used"
|
return "No assets available"
|
||||||
|
|
||||||
case .invalidImage:
|
|
||||||
return "An invalid image was received"
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "An unknown error occured"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,10 +52,11 @@ struct Asset: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchFilters: Codable {
|
struct SearchFilter: Codable {
|
||||||
var type: AssetType = .image
|
var type = AssetType.image
|
||||||
let size: Int
|
var size = 1
|
||||||
var albumIds: [String] = []
|
var albumIds: [String] = []
|
||||||
|
var isFavorite: Bool? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryResult: Codable {
|
struct MemoryResult: Codable {
|
||||||
@@ -70,12 +71,35 @@ struct MemoryResult: Codable {
|
|||||||
let data: MemoryData
|
let data: MemoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Album: Codable {
|
struct Album: Codable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
let albumName: 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
|
// MARK: API
|
||||||
|
|
||||||
@@ -132,7 +156,8 @@ class ImmichAPI {
|
|||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilters) async throws
|
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||||
|
async throws
|
||||||
-> [Asset]
|
-> [Asset]
|
||||||
{
|
{
|
||||||
// get URL
|
// get URL
|
||||||
@@ -178,7 +203,7 @@ class ImmichAPI {
|
|||||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
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 thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||||
|
|
||||||
@@ -199,7 +224,7 @@ class ImmichAPI {
|
|||||||
|
|
||||||
let decodeOptions: [NSString: Any] = [
|
let decodeOptions: [NSString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
kCGImageSourceThumbnailMaxPixelSize: 512,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -23,26 +23,27 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
Task {
|
Task {
|
||||||
guard let api = try? await ImmichAPI() else {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for memory in memories {
|
for memory in memories {
|
||||||
if let asset = memory.assets.first(where: { $0.type == .image }),
|
if let asset = memory.assets.first(where: { $0.type == .image }),
|
||||||
var entry = try? await ImageEntry.build(
|
let entry = try? await ImageEntry.build(
|
||||||
api: api,
|
api: api,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
dateOffset: 0,
|
dateOffset: 0,
|
||||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
entry.resize()
|
|
||||||
completion(entry)
|
completion(entry)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -50,20 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
// fallback to random image
|
// fallback to random image
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults().first,
|
||||||
with: SearchFilters(size: 1)
|
let imageEntry = try? await ImageEntry.build(
|
||||||
).first,
|
|
||||||
var imageEntry = try? await ImageEntry.build(
|
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imageEntry.resize()
|
|
||||||
completion(imageEntry)
|
completion(imageEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +78,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
completion(
|
completion(
|
||||||
ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -119,25 +117,28 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
// If we didnt add any memory images (some failure occured or no images in memory),
|
// If we didnt add any memory images (some failure occured or no images in memory),
|
||||||
// default to 12 hours of random photos
|
// default to 12 hours of random photos
|
||||||
if entries.count == 0 {
|
if entries.count == 0 {
|
||||||
entries.append(
|
// this must be a do/catch since we need to
|
||||||
contentsOf: (try? await generateRandomEntries(
|
// distinguish between a network fail and an empty search
|
||||||
|
do {
|
||||||
|
let search = try await generateRandomEntries(
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12
|
count: 12
|
||||||
)) ?? []
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fail to fetch images, we still want to add an entry
|
// Load or save a cached asset for when network conditions are bad
|
||||||
// with a nil image and an error
|
if search.count == 0 {
|
||||||
if entries.count == 0 {
|
completion(
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey))
|
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||||
return
|
)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Resize all images to something that can be stored by iOS
|
entries.append(contentsOf: search)
|
||||||
for i in entries.indices {
|
} catch {
|
||||||
entries[i].resize()
|
completion(ImageEntry.handleError(for: cacheKey))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// cache the last image
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|||||||
|
|
||||||
struct AlbumQuery: EntityQuery {
|
struct AlbumQuery: EntityQuery {
|
||||||
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
||||||
// use cached albums to search
|
return await suggestedEntities().filter {
|
||||||
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
|
||||||
albums.insert(NO_ALBUM, at: 0)
|
|
||||||
|
|
||||||
return albums.filter {
|
|
||||||
identifiers.contains($0.id)
|
identifiers.contains($0.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func suggestedEntities() async throws -> [Album] {
|
func suggestedEntities() async -> [Album] {
|
||||||
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||||
albums.insert(NO_ALBUM, at: 0)
|
|
||||||
|
|
||||||
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 {
|
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource { "Select Album" }
|
static var title: LocalizedStringResource { "Select Album" }
|
||||||
static var description: IntentDescription {
|
static var description: IntentDescription {
|
||||||
@@ -64,26 +63,25 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
-> ImageEntry
|
-> ImageEntry
|
||||||
{
|
{
|
||||||
let cacheKey = "random_none_\(context.family.rawValue)"
|
let cacheKey = "random_none_\(context.family.rawValue)"
|
||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
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
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults(
|
||||||
with: SearchFilters(size: 1)
|
with: Album.NONE.filter
|
||||||
).first,
|
).first,
|
||||||
var entry = try? await ImageEntry.build(
|
let entry = try? await ImageEntry.build(
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey).entries.first!
|
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.resize()
|
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,52 +95,36 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
// nil if album is NONE or nil
|
// nil if album is NONE or nil
|
||||||
let albumId =
|
let album = configuration.album ?? Album.NONE
|
||||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
let albumName = album.isVirtual ? nil : album.albumName
|
||||||
let albumName: String? =
|
|
||||||
albumId != nil ? configuration.album?.albumName : nil
|
|
||||||
|
|
||||||
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
|
// If we don't have a server config, return an entry with an error
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
return ImageEntry.handleError(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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build entries
|
// build entries
|
||||||
entries.append(
|
// this must be a do/catch since we need to
|
||||||
contentsOf: (try? await generateRandomEntries(
|
// distinguish between a network fail and an empty search
|
||||||
|
do {
|
||||||
|
let search = try await generateRandomEntries(
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12,
|
count: 12,
|
||||||
albumId: albumId,
|
filter: album.filter,
|
||||||
subtitle: configuration.showAlbumName ? albumName : nil
|
subtitle: configuration.showAlbumName ? albumName : nil
|
||||||
))
|
)
|
||||||
?? []
|
|
||||||
)
|
|
||||||
|
|
||||||
// Load or save a cached asset for when network conditions are bad
|
// Load or save a cached asset for when network conditions are bad
|
||||||
if entries.count == 0 {
|
if search.count == 0 {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey)
|
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize all images to something that can be stored by iOS
|
entries.append(contentsOf: search)
|
||||||
for i in entries.indices {
|
} catch {
|
||||||
entries[i].resize()
|
return ImageEntry.handleError(for: cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// cache the last image
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}
|
|
||||||
@@ -16,6 +16,11 @@ const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
|||||||
// Secure storage keys
|
// Secure storage keys
|
||||||
const String kSecuredPinCode = "secured_pin_code";
|
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
|
// Timeline constants
|
||||||
const int kTimelineNoneSegmentSize = 120;
|
const int kTimelineNoneSegmentSize = 120;
|
||||||
const int kTimelineAssetLoadBatchSize = 256;
|
const int kTimelineAssetLoadBatchSize = 256;
|
||||||
@@ -28,7 +33,11 @@ const String appShareGroupId = "group.app.immich.share";
|
|||||||
|
|
||||||
// add widget identifiers here for new widgets
|
// add widget identifiers here for new widgets
|
||||||
// these are used to force a widget refresh
|
// these are used to force a widget refresh
|
||||||
const List<String> kWidgetNames = [
|
// (iOSName, androidFQDN)
|
||||||
'com.immich.widget.random',
|
const List<(String, String)> kWidgetNames = [
|
||||||
'com.immich.widget.memory',
|
('com.immich.widget.random', 'app.alextran.immich.widget.RandomReceiver'),
|
||||||
|
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const double kUploadStatusFailed = -1.0;
|
||||||
|
const double kUploadStatusCanceled = -2.0;
|
||||||
|
|||||||
@@ -86,4 +86,32 @@ class RemoteAlbum {
|
|||||||
assetCount.hashCode ^
|
assetCount.hashCode ^
|
||||||
ownerName.hashCode;
|
ownerName.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RemoteAlbum copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? ownerId,
|
||||||
|
String? description,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
bool? isActivityEnabled,
|
||||||
|
AlbumAssetOrder? order,
|
||||||
|
int? assetCount,
|
||||||
|
String? ownerName,
|
||||||
|
}) {
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
description: description ?? this.description,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
||||||
|
order: order ?? this.order,
|
||||||
|
assetCount: assetCount ?? this.assetCount,
|
||||||
|
ownerName: ownerName ?? this.ownerName,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class LocalAsset extends BaseAsset {
|
|||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not checking for remoteId here
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! LocalAsset) return false;
|
if (other is! LocalAsset) return false;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final String? thumbHash;
|
final String? thumbHash;
|
||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
|
final String? stackId;
|
||||||
|
final int stackCount;
|
||||||
|
|
||||||
const RemoteAsset({
|
const RemoteAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -31,6 +33,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
super.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
|
this.stackId,
|
||||||
|
this.stackCount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -56,9 +60,14 @@ class RemoteAsset extends BaseAsset {
|
|||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
thumbHash: ${thumbHash ?? "<NA>"},
|
thumbHash: ${thumbHash ?? "<NA>"},
|
||||||
visibility: $visibility,
|
visibility: $visibility,
|
||||||
|
stackId: ${stackId ?? "<NA>"},
|
||||||
|
stackCount: $stackCount,
|
||||||
|
checksum: $checksum,
|
||||||
|
livePhotoVideoId: ${livePhotoVideoId ?? "<NA>"},
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not checking for localId here
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! RemoteAsset) return false;
|
if (other is! RemoteAsset) return false;
|
||||||
@@ -67,7 +76,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
id == other.id &&
|
id == other.id &&
|
||||||
ownerId == other.ownerId &&
|
ownerId == other.ownerId &&
|
||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility;
|
visibility == other.visibility &&
|
||||||
|
stackId == other.stackId &&
|
||||||
|
stackCount == other.stackCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -77,7 +88,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
ownerId.hashCode ^
|
ownerId.hashCode ^
|
||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode;
|
visibility.hashCode ^
|
||||||
|
stackId.hashCode ^
|
||||||
|
stackCount.hashCode;
|
||||||
|
|
||||||
RemoteAsset copyWith({
|
RemoteAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -95,6 +108,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
String? thumbHash,
|
String? thumbHash,
|
||||||
AssetVisibility? visibility,
|
AssetVisibility? visibility,
|
||||||
String? livePhotoVideoId,
|
String? livePhotoVideoId,
|
||||||
|
String? stackId,
|
||||||
|
int? stackCount,
|
||||||
}) {
|
}) {
|
||||||
return RemoteAsset(
|
return RemoteAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -112,6 +127,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash: thumbHash ?? this.thumbHash,
|
thumbHash: thumbHash ?? this.thumbHash,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
|
stackId: stackId ?? this.stackId,
|
||||||
|
stackCount: stackCount ?? this.stackCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,21 @@ class DriftMemory {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
|
return '''Memory {
|
||||||
|
id: $id,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
updatedAt: $updatedAt,
|
||||||
|
deletedAt: ${deletedAt ?? "<NA>"},
|
||||||
|
ownerId: $ownerId,
|
||||||
|
type: $type,
|
||||||
|
data: $data,
|
||||||
|
isSaved: $isSaved,
|
||||||
|
memoryAt: $memoryAt,
|
||||||
|
seenAt: ${seenAt ?? "<NA>"},
|
||||||
|
showAt: ${showAt ?? "<NA>"},
|
||||||
|
hideAt: ${hideAt ?? "<NA>"},
|
||||||
|
assets: $assets
|
||||||
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class Person {
|
// TODO: Remove PersonDto once Isar is removed
|
||||||
const Person({
|
class PersonDto {
|
||||||
|
const PersonDto({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.birthDate,
|
this.birthDate,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
@@ -22,7 +23,7 @@ class Person {
|
|||||||
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
Person copyWith({
|
PersonDto copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
DateTime? birthDate,
|
DateTime? birthDate,
|
||||||
bool? isHidden,
|
bool? isHidden,
|
||||||
@@ -30,7 +31,7 @@ class Person {
|
|||||||
String? thumbnailPath,
|
String? thumbnailPath,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
}) {
|
}) {
|
||||||
return Person(
|
return PersonDto(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
birthDate: birthDate ?? this.birthDate,
|
birthDate: birthDate ?? this.birthDate,
|
||||||
isHidden: isHidden ?? this.isHidden,
|
isHidden: isHidden ?? this.isHidden,
|
||||||
@@ -51,8 +52,8 @@ class Person {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Person.fromMap(Map<String, dynamic> map) {
|
factory PersonDto.fromMap(Map<String, dynamic> map) {
|
||||||
return Person(
|
return PersonDto(
|
||||||
id: map['id'] as String,
|
id: map['id'] as String,
|
||||||
birthDate: map['birthDate'] != null
|
birthDate: map['birthDate'] != null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int)
|
||||||
@@ -68,11 +69,11 @@ class Person {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory Person.fromJson(String source) =>
|
factory PersonDto.fromJson(String source) =>
|
||||||
Person.fromMap(json.decode(source) as Map<String, dynamic>);
|
PersonDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant Person other) {
|
bool operator ==(covariant PersonDto other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other.id == id &&
|
return other.id == id &&
|
||||||
@@ -93,3 +94,109 @@ class Person {
|
|||||||
updatedAt.hashCode;
|
updatedAt.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model for a person stored in the server
|
||||||
|
class Person {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String name;
|
||||||
|
final String? faceAssetId;
|
||||||
|
final String thumbnailPath;
|
||||||
|
final bool isFavorite;
|
||||||
|
final bool isHidden;
|
||||||
|
final String? color;
|
||||||
|
final DateTime? birthDate;
|
||||||
|
|
||||||
|
const Person({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.name,
|
||||||
|
this.faceAssetId,
|
||||||
|
required this.thumbnailPath,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.isHidden,
|
||||||
|
required this.color,
|
||||||
|
this.birthDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
Person copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? name,
|
||||||
|
String? faceAssetId,
|
||||||
|
String? thumbnailPath,
|
||||||
|
bool? isFavorite,
|
||||||
|
bool? isHidden,
|
||||||
|
String? color,
|
||||||
|
DateTime? birthDate,
|
||||||
|
}) {
|
||||||
|
return Person(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
name: name ?? this.name,
|
||||||
|
faceAssetId: faceAssetId ?? this.faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
color: color ?? this.color,
|
||||||
|
birthDate: birthDate ?? this.birthDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''Person {
|
||||||
|
id: $id,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
updatedAt: $updatedAt,
|
||||||
|
ownerId: $ownerId,
|
||||||
|
name: $name,
|
||||||
|
faceAssetId: ${faceAssetId ?? "<NA>"},
|
||||||
|
thumbnailPath: $thumbnailPath,
|
||||||
|
isFavorite: $isFavorite,
|
||||||
|
isHidden: $isHidden,
|
||||||
|
color: ${color ?? "<NA>"},
|
||||||
|
birthDate: ${birthDate ?? "<NA>"}
|
||||||
|
}''';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant Person other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.updatedAt == updatedAt &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.name == name &&
|
||||||
|
other.faceAssetId == faceAssetId &&
|
||||||
|
other.thumbnailPath == thumbnailPath &&
|
||||||
|
other.isFavorite == isFavorite &&
|
||||||
|
other.isHidden == isHidden &&
|
||||||
|
other.color == color &&
|
||||||
|
other.birthDate == birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
updatedAt.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
faceAssetId.hashCode ^
|
||||||
|
thumbnailPath.hashCode ^
|
||||||
|
isFavorite.hashCode ^
|
||||||
|
isHidden.hashCode ^
|
||||||
|
color.hashCode ^
|
||||||
|
birthDate.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
|
||||||
|
class SearchResult {
|
||||||
|
final List<BaseAsset> assets;
|
||||||
|
final int? nextPage;
|
||||||
|
|
||||||
|
const SearchResult({
|
||||||
|
required this.assets,
|
||||||
|
this.nextPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
int get totalAssets => assets.length;
|
||||||
|
|
||||||
|
SearchResult copyWith({
|
||||||
|
List<BaseAsset>? assets,
|
||||||
|
int? nextPage,
|
||||||
|
}) {
|
||||||
|
return SearchResult(
|
||||||
|
assets: assets ?? this.assets,
|
||||||
|
nextPage: nextPage ?? this.nextPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant SearchResult other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return listEquals(other.assets, assets) && other.nextPage == nextPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assets.hashCode ^ nextPage.hashCode;
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
// Model for a stack stored in the server
|
// Model for a stack stored in the server
|
||||||
class Stack {
|
class Stack {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -32,34 +30,15 @@ class Stack {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': id,
|
|
||||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
|
||||||
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
|
||||||
'ownerId': ownerId,
|
|
||||||
'primaryAssetId': primaryAssetId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory Stack.fromMap(Map<String, dynamic> map) {
|
|
||||||
return Stack(
|
|
||||||
id: map['id'] as String,
|
|
||||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
|
||||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
|
||||||
ownerId: map['ownerId'] as String,
|
|
||||||
primaryAssetId: map['primaryAssetId'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory Stack.fromJson(String source) =>
|
|
||||||
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
@@ -82,3 +61,27 @@ class Stack {
|
|||||||
primaryAssetId.hashCode;
|
primaryAssetId.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StackResponse {
|
||||||
|
final String id;
|
||||||
|
final String primaryAssetId;
|
||||||
|
final List<String> assetIds;
|
||||||
|
|
||||||
|
const StackResponse({
|
||||||
|
required this.id,
|
||||||
|
required this.primaryAssetId,
|
||||||
|
required this.assetIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant StackResponse other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.primaryAssetId == primaryAssetId &&
|
||||||
|
other.assetIds == assetIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode ^ primaryAssetId.hashCode ^ assetIds.hashCode;
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ enum StoreKey<T> {
|
|||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000);
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
|
betaPromptShown<bool>._(1001),
|
||||||
|
betaTimeline<bool>._(1002),
|
||||||
|
enableBackup<bool>._(1003);
|
||||||
|
|
||||||
const StoreKey._(this.id);
|
const StoreKey._(this.id);
|
||||||
final int id;
|
final int id;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
|
||||||
enum GroupAssetsBy {
|
enum GroupAssetsBy {
|
||||||
day,
|
day,
|
||||||
month,
|
month,
|
||||||
@@ -38,3 +40,7 @@ class TimeBucket extends Bucket {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => super.hashCode ^ date.hashCode;
|
int get hashCode => super.hashCode ^ date.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimelineReloadEvent extends Event {
|
||||||
|
const TimelineReloadEvent();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||||
|
|
||||||
// TODO: Rename to User once Isar is removed
|
// TODO: Rename to User once Isar is removed
|
||||||
@@ -123,3 +126,88 @@ quotaSizeInBytes: $quotaSizeInBytes,
|
|||||||
quotaUsageInBytes.hashCode ^
|
quotaUsageInBytes.hashCode ^
|
||||||
quotaSizeInBytes.hashCode;
|
quotaSizeInBytes.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PartnerUserDto {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String name;
|
||||||
|
final bool inTimeline;
|
||||||
|
|
||||||
|
final String? profileImagePath;
|
||||||
|
|
||||||
|
const PartnerUserDto({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.name,
|
||||||
|
required this.inTimeline,
|
||||||
|
this.profileImagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
PartnerUserDto copyWith({
|
||||||
|
String? id,
|
||||||
|
String? email,
|
||||||
|
String? name,
|
||||||
|
bool? inTimeline,
|
||||||
|
String? profileImagePath,
|
||||||
|
}) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: id ?? this.id,
|
||||||
|
email: email ?? this.email,
|
||||||
|
name: name ?? this.name,
|
||||||
|
inTimeline: inTimeline ?? this.inTimeline,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'email': email,
|
||||||
|
'name': name,
|
||||||
|
'inTimeline': inTimeline,
|
||||||
|
'profileImagePath': profileImagePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PartnerUserDto.fromMap(Map<String, dynamic> map) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: map['id'] as String,
|
||||||
|
email: map['email'] as String,
|
||||||
|
name: map['name'] as String,
|
||||||
|
inTimeline: map['inTimeline'] as bool,
|
||||||
|
profileImagePath: map['profileImagePath'] != null
|
||||||
|
? map['profileImagePath'] as String
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory PartnerUserDto.fromJson(String source) =>
|
||||||
|
PartnerUserDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant PartnerUserDto other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.email == email &&
|
||||||
|
other.name == name &&
|
||||||
|
other.inTimeline == inTimeline &&
|
||||||
|
other.profileImagePath == profileImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
email.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
inTimeline.hashCode ^
|
||||||
|
profileImagePath.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ class AssetService {
|
|||||||
: _remoteAssetRepository.watchAsset(id);
|
: _remoteAssetRepository.watchAsset(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
|
||||||
|
if (asset.stackId == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _remoteAssetRepository.getStackChildren(asset).then((assets) {
|
||||||
|
// Include the primary asset in the stack as the first item
|
||||||
|
return [asset, ...assets];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||||
if (!asset.hasRemote) {
|
if (!asset.hasRemote) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ class LocalAlbumService {
|
|||||||
|
|
||||||
const LocalAlbumService(this._repository);
|
const LocalAlbumService(this._repository);
|
||||||
|
|
||||||
Future<List<LocalAlbum>> getAll() {
|
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
|
||||||
return _repository.getAll();
|
return _repository.getAll(sortBy: sortBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LocalAsset?> getThumbnail(String albumId) {
|
Future<LocalAsset?> getThumbnail(String albumId) {
|
||||||
return _repository.getThumbnail(albumId);
|
return _repository.getThumbnail(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> update(LocalAlbum album) {
|
||||||
|
return _repository.upsert(album);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||||
|
|
||||||
|
class DriftPartnerService {
|
||||||
|
final DriftPartnerRepository _driftPartnerRepository;
|
||||||
|
final PartnerApiRepository _partnerApiRepository;
|
||||||
|
|
||||||
|
const DriftPartnerService(
|
||||||
|
this._driftPartnerRepository,
|
||||||
|
this._partnerApiRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getSharedWith(String userId) {
|
||||||
|
return _driftPartnerRepository.getSharedWith(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||||
|
return _driftPartnerRepository.getSharedBy(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getAvailablePartners(
|
||||||
|
String currentUserId,
|
||||||
|
) async {
|
||||||
|
final otherUsers =
|
||||||
|
await _driftPartnerRepository.getAvailablePartners(currentUserId);
|
||||||
|
final currentPartners =
|
||||||
|
await _driftPartnerRepository.getSharedBy(currentUserId);
|
||||||
|
final available = otherUsers.where((user) {
|
||||||
|
return !currentPartners.any((partner) => partner.id == user.id);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
|
||||||
|
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
|
||||||
|
if (partner == null) {
|
||||||
|
debugPrint("Partner not found: $partnerId for user: $userId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _partnerApiRepository.update(
|
||||||
|
partnerId,
|
||||||
|
inTimeline: !partner.inTimeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _driftPartnerRepository.toggleShowInTimeline(partner, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPartner(String partnerId, String userId) async {
|
||||||
|
await _partnerApiRepository.create(partnerId);
|
||||||
|
await _driftPartnerRepository.create(partnerId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removePartner(String partnerId, String userId) async {
|
||||||
|
await _partnerApiRepository.delete(partnerId);
|
||||||
|
await _driftPartnerRepository.delete(partnerId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/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/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
@@ -10,6 +14,10 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||||
|
|
||||||
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
|
return _repository.watchAlbum(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> getAll() {
|
Future<List<RemoteAlbum>> getAll() {
|
||||||
return _repository.getAll();
|
return _repository.getAll();
|
||||||
}
|
}
|
||||||
@@ -75,4 +83,68 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum> updateAlbum(
|
||||||
|
String albumId, {
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
bool? isActivityEnabled,
|
||||||
|
AlbumAssetOrder? order,
|
||||||
|
}) async {
|
||||||
|
final updatedAlbum = await _albumApiRepository.updateAlbum(
|
||||||
|
albumId,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
thumbnailAssetId: thumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled,
|
||||||
|
order: order,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the local database
|
||||||
|
await _repository.update(updatedAlbum);
|
||||||
|
|
||||||
|
return updatedAlbum;
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||||
|
return _repository.getDateRange(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<UserDto>> getSharedUsers(String albumId) {
|
||||||
|
return _repository.getSharedUsers(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
|
return _repository.getAssets(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> addAssets({
|
||||||
|
required String albumId,
|
||||||
|
required List<String> assetIds,
|
||||||
|
}) async {
|
||||||
|
final album = await _albumApiRepository.addAssets(
|
||||||
|
albumId,
|
||||||
|
assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _repository.addAssets(albumId, album.added);
|
||||||
|
|
||||||
|
return album.added.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
|
await _albumApiRepository.deleteAlbum(albumId);
|
||||||
|
|
||||||
|
await _repository.deleteAlbum(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addUsers({
|
||||||
|
required String albumId,
|
||||||
|
required List<String> userIds,
|
||||||
|
}) async {
|
||||||
|
await _albumApiRepository.addUsers(albumId, userIds);
|
||||||
|
|
||||||
|
return _repository.addUsers(albumId, userIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||||
|
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||||
|
|
||||||
|
class SearchService {
|
||||||
|
final _log = Logger("SearchService");
|
||||||
|
final SearchApiRepository _searchApiRepository;
|
||||||
|
|
||||||
|
SearchService(this._searchApiRepository);
|
||||||
|
|
||||||
|
Future<List<String>?> getSearchSuggestions(
|
||||||
|
SearchSuggestionType type, {
|
||||||
|
String? country,
|
||||||
|
String? state,
|
||||||
|
String? make,
|
||||||
|
String? model,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _searchApiRepository.getSearchSuggestions(
|
||||||
|
type,
|
||||||
|
country: country,
|
||||||
|
state: state,
|
||||||
|
make: make,
|
||||||
|
model: model,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_log.warning("Failed to get search suggestions", e);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SearchResult?> search(SearchFilter filter, int page) async {
|
||||||
|
try {
|
||||||
|
final response = await _searchApiRepository.search(filter, page);
|
||||||
|
|
||||||
|
if (response == null || response.assets.items.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SearchResult(
|
||||||
|
assets: response.assets.items.map((e) => e.toDto()).toList(),
|
||||||
|
nextPage: response.assets.nextPage?.toInt(),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_log.severe("Failed to search for assets", error, stackTrace);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on AssetResponseDto {
|
||||||
|
RemoteAsset toDto() {
|
||||||
|
return RemoteAsset(
|
||||||
|
id: id,
|
||||||
|
name: originalFileName,
|
||||||
|
checksum: checksum,
|
||||||
|
createdAt: fileCreatedAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
visibility: switch (visibility) {
|
||||||
|
api.AssetVisibility.timeline => AssetVisibility.timeline,
|
||||||
|
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
||||||
|
api.AssetVisibility.archive => AssetVisibility.archive,
|
||||||
|
api.AssetVisibility.locked => AssetVisibility.locked,
|
||||||
|
_ => AssetVisibility.timeline,
|
||||||
|
},
|
||||||
|
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
|
||||||
|
height: exifInfo?.exifImageHeight?.toInt(),
|
||||||
|
width: exifInfo?.exifImageWidth?.toInt(),
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
|
thumbHash: thumbhash,
|
||||||
|
localId: null,
|
||||||
|
type: type.toAssetType(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on AssetTypeEnum {
|
||||||
|
AssetType toAssetType() => switch (this) {
|
||||||
|
AssetTypeEnum.IMAGE => AssetType.image,
|
||||||
|
AssetTypeEnum.VIDEO => AssetType.video,
|
||||||
|
AssetTypeEnum.AUDIO => AssetType.audio,
|
||||||
|
AssetTypeEnum.OTHER => AssetType.other,
|
||||||
|
_ => throw Exception('Unknown AssetType value: $this'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -93,6 +93,8 @@ class StoreService {
|
|||||||
await _storeRepository.deleteAll();
|
await _storeRepository.deleteAll();
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreKeyNotFoundException implements Exception {
|
class StoreKeyNotFoundException implements Exception {
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deleteUserMetadatasV1(
|
return _syncStreamRepository.deleteUserMetadatasV1(
|
||||||
data.cast(),
|
data.cast(),
|
||||||
);
|
);
|
||||||
|
case SyncEntityType.personV1:
|
||||||
|
return _syncStreamRepository.updatePeopleV1(data.cast());
|
||||||
|
case SyncEntityType.personDeleteV1:
|
||||||
|
return _syncStreamRepository.deletePeopleV1(data.cast());
|
||||||
default:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_logger.warning("Unknown sync data type: $type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class TimelineFactory {
|
|||||||
|
|
||||||
TimelineService place(String place) =>
|
TimelineService place(String place) =>
|
||||||
TimelineService(_timelineRepository.place(place, groupBy));
|
TimelineService(_timelineRepository.place(place, groupBy));
|
||||||
|
|
||||||
|
TimelineService fromAssets(List<BaseAsset> assets) =>
|
||||||
|
TimelineService(_timelineRepository.fromAssets(assets));
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
|
|||||||
@@ -4,13 +4,24 @@ import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
|||||||
import 'package:immich_mobile/utils/isolate.dart';
|
import 'package:immich_mobile/utils/isolate.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
|
typedef SyncCallback = void Function();
|
||||||
|
typedef SyncErrorCallback = void Function(String error);
|
||||||
|
|
||||||
class BackgroundSyncManager {
|
class BackgroundSyncManager {
|
||||||
|
final SyncCallback? onRemoteSyncStart;
|
||||||
|
final SyncCallback? onRemoteSyncComplete;
|
||||||
|
final SyncErrorCallback? onRemoteSyncError;
|
||||||
|
|
||||||
Cancelable<void>? _syncTask;
|
Cancelable<void>? _syncTask;
|
||||||
Cancelable<void>? _syncWebsocketTask;
|
Cancelable<void>? _syncWebsocketTask;
|
||||||
Cancelable<void>? _deviceAlbumSyncTask;
|
Cancelable<void>? _deviceAlbumSyncTask;
|
||||||
Cancelable<void>? _hashTask;
|
Cancelable<void>? _hashTask;
|
||||||
|
|
||||||
BackgroundSyncManager();
|
BackgroundSyncManager({
|
||||||
|
this.onRemoteSyncStart,
|
||||||
|
this.onRemoteSyncComplete,
|
||||||
|
this.onRemoteSyncError,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> cancel() {
|
Future<void> cancel() {
|
||||||
final futures = <Future>[];
|
final futures = <Future>[];
|
||||||
@@ -72,10 +83,16 @@ class BackgroundSyncManager {
|
|||||||
return _syncTask!.future;
|
return _syncTask!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRemoteSyncStart?.call();
|
||||||
|
|
||||||
_syncTask = runInIsolateGentle(
|
_syncTask = runInIsolateGentle(
|
||||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||||
);
|
);
|
||||||
return _syncTask!.whenComplete(() {
|
return _syncTask!.whenComplete(() {
|
||||||
|
onRemoteSyncComplete?.call();
|
||||||
|
_syncTask = null;
|
||||||
|
}).catchError((error) {
|
||||||
|
onRemoteSyncError?.call(error.toString());
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -84,14 +101,18 @@ class BackgroundSyncManager {
|
|||||||
if (_syncWebsocketTask != null) {
|
if (_syncWebsocketTask != null) {
|
||||||
return _syncWebsocketTask!.future;
|
return _syncWebsocketTask!.future;
|
||||||
}
|
}
|
||||||
|
_syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData);
|
||||||
_syncWebsocketTask = runInIsolateGentle(
|
|
||||||
computation: (ref) => ref
|
|
||||||
.read(syncStreamServiceProvider)
|
|
||||||
.handleWsAssetUploadReadyV1Batch(batchData),
|
|
||||||
);
|
|
||||||
return _syncWebsocketTask!.whenComplete(() {
|
return _syncWebsocketTask!.whenComplete(() {
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cancelable<void> _handleWsAssetUploadReadyV1Batch(
|
||||||
|
List<dynamic> batchData,
|
||||||
|
) =>
|
||||||
|
runInIsolateGentle(
|
||||||
|
computation: (ref) => ref
|
||||||
|
.read(syncStreamServiceProvider)
|
||||||
|
.handleWsAssetUploadReadyV1Batch(batchData),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
sealed class Event {
|
class Event {
|
||||||
const Event();
|
const Event();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineReloadEvent extends Event {
|
|
||||||
const TimelineReloadEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewerOpenBottomSheetEvent extends Event {
|
|
||||||
const ViewerOpenBottomSheetEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class EventStream {
|
class EventStream {
|
||||||
EventStream._();
|
EventStream._();
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ class BackupAlbum {
|
|||||||
BackupAlbum(this.id, this.lastBackup, this.selection);
|
BackupAlbum(this.id, this.lastBackup, this.selection);
|
||||||
|
|
||||||
Id get isarId => fastHash(id);
|
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 {
|
enum BackupSelection {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ extension ContextHelper on BuildContext {
|
|||||||
|
|
||||||
// Returns the current Primary color of the Theme
|
// Returns the current Primary color of the Theme
|
||||||
Color get primaryColor => themeData.colorScheme.primary;
|
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
|
// Returns the Scaffold background color of the Theme
|
||||||
Color get scaffoldBackgroundColor => colorScheme.surface;
|
Color get scaffoldBackgroundColor => colorScheme.surface;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
extension TimeAgoExtension on DateTime {
|
extension TimeAgoExtension on DateTime {
|
||||||
/// Displays the time difference of this [DateTime] object to the current time as a [String]
|
/// Displays the time difference of this [DateTime] object to the current time as a [String]
|
||||||
String timeAgo({bool numericDates = true}) {
|
String timeAgo({bool numericDates = true}) {
|
||||||
@@ -35,3 +38,56 @@ extension TimeAgoExtension on DateTime {
|
|||||||
return '${(difference.inDays / 365).floor()} years ago';
|
return '${(difference.inDays / 365).floor()} years ago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extension to format date ranges according to UI requirements
|
||||||
|
extension DateRangeFormatting on DateTime {
|
||||||
|
/// Formats a date range according to specific rules:
|
||||||
|
/// - Single date of this year: "Aug 28"
|
||||||
|
/// - Single date of other year: "Aug 28, 2023"
|
||||||
|
/// - Date range of this year: "Mar 23-May 31"
|
||||||
|
/// - Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||||
|
/// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||||
|
static String formatDateRange(
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
Locale? locale,
|
||||||
|
) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final currentYear = now.year;
|
||||||
|
final localeString = locale?.toString() ?? 'en_US';
|
||||||
|
|
||||||
|
// Check if it's a single date (same day)
|
||||||
|
if (startDate.year == endDate.year &&
|
||||||
|
startDate.month == endDate.month &&
|
||||||
|
startDate.day == endDate.day) {
|
||||||
|
if (startDate.year == currentYear) {
|
||||||
|
// Single date of this year: "Aug 28"
|
||||||
|
return DateFormat.MMMd(localeString).format(startDate);
|
||||||
|
} else {
|
||||||
|
// Single date of other year: "Aug 28, 2023"
|
||||||
|
return DateFormat.yMMMd(localeString).format(startDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a date range
|
||||||
|
if (startDate.year == endDate.year) {
|
||||||
|
// Same year
|
||||||
|
if (startDate.year == currentYear) {
|
||||||
|
// Date range of this year: "Mar 23-May 31"
|
||||||
|
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||||
|
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||||
|
return '$startFormatted - $endFormatted';
|
||||||
|
} else {
|
||||||
|
// Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||||
|
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||||
|
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||||
|
return '$startFormatted - $endFormatted, ${startDate.year}';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||||
|
final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
|
||||||
|
final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
|
||||||
|
return '$startFormatted - $endFormatted';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'remote_asset.entity.dart';
|
import 'remote_asset.entity.dart';
|
||||||
import 'local_asset.entity.dart';
|
import 'local_asset.entity.dart';
|
||||||
|
import 'stack.entity.dart';
|
||||||
|
|
||||||
mergedAsset: SELECT * FROM
|
mergedAsset: SELECT * FROM
|
||||||
(
|
(
|
||||||
@@ -18,13 +19,33 @@ mergedAsset: SELECT * FROM
|
|||||||
rae.checksum,
|
rae.checksum,
|
||||||
rae.owner_id,
|
rae.owner_id,
|
||||||
rae.live_photo_video_id,
|
rae.live_photo_video_id,
|
||||||
0 as orientation
|
0 as orientation,
|
||||||
|
rae.stack_id,
|
||||||
|
COALESCE(stack_count.total_count, 0) AS stack_count
|
||||||
FROM
|
FROM
|
||||||
remote_asset_entity rae
|
remote_asset_entity rae
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
|
LEFT JOIN
|
||||||
|
stack_entity se ON rae.stack_id = se.id
|
||||||
|
LEFT JOIN
|
||||||
|
(SELECT
|
||||||
|
stack_id,
|
||||||
|
COUNT(*) AS total_count
|
||||||
|
FROM remote_asset_entity
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND visibility = 0
|
||||||
|
AND stack_id IS NOT NULL
|
||||||
|
GROUP BY stack_id
|
||||||
|
) AS stack_count ON rae.stack_id = stack_count.stack_id
|
||||||
WHERE
|
WHERE
|
||||||
rae.deleted_at IS NULL 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
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
NULL as remote_id,
|
NULL as remote_id,
|
||||||
@@ -41,7 +62,9 @@ mergedAsset: SELECT * FROM
|
|||||||
lae.checksum,
|
lae.checksum,
|
||||||
NULL as owner_id,
|
NULL as owner_id,
|
||||||
NULL as live_photo_video_id,
|
NULL as live_photo_video_id,
|
||||||
lae.orientation
|
lae.orientation,
|
||||||
|
NULL as stack_id,
|
||||||
|
0 AS stack_count
|
||||||
FROM
|
FROM
|
||||||
local_asset_entity lae
|
local_asset_entity lae
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -68,8 +91,16 @@ FROM
|
|||||||
remote_asset_entity rae
|
remote_asset_entity rae
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
|
LEFT JOIN
|
||||||
|
stack_entity se ON rae.stack_id = se.id
|
||||||
WHERE
|
WHERE
|
||||||
rae.deleted_at IS NULL 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
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
lae.name,
|
lae.name,
|
||||||
|
|||||||
+15
-2
@@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
|||||||
as i3;
|
as i3;
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||||
as i4;
|
as i4;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
|
||||||
class MergedAssetDrift extends i1.ModularAccessor {
|
class MergedAssetDrift extends i1.ModularAccessor {
|
||||||
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||||
@@ -18,7 +20,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
||||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation 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: [
|
variables: [
|
||||||
for (var $ in var1) i0.Variable<String>($),
|
for (var $ in var1) i0.Variable<String>($),
|
||||||
...generatedlimit.introducedVariables
|
...generatedlimit.introducedVariables
|
||||||
@@ -26,6 +28,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
readsFrom: {
|
readsFrom: {
|
||||||
remoteAssetEntity,
|
remoteAssetEntity,
|
||||||
localAssetEntity,
|
localAssetEntity,
|
||||||
|
stackEntity,
|
||||||
...generatedlimit.watchedTables,
|
...generatedlimit.watchedTables,
|
||||||
}).map((i0.QueryRow row) => MergedAssetResult(
|
}).map((i0.QueryRow row) => MergedAssetResult(
|
||||||
remoteId: row.readNullable<String>('remote_id'),
|
remoteId: row.readNullable<String>('remote_id'),
|
||||||
@@ -44,6 +47,8 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
ownerId: row.readNullable<String>('owner_id'),
|
ownerId: row.readNullable<String>('owner_id'),
|
||||||
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
|
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
|
||||||
orientation: row.read<int>('orientation'),
|
orientation: row.read<int>('orientation'),
|
||||||
|
stackId: row.readNullable<String>('stack_id'),
|
||||||
|
stackCount: row.read<int>('stack_count'),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +58,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
||||||
$arrayStartIndex += var2.length;
|
$arrayStartIndex += var2.length;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum 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: [
|
variables: [
|
||||||
i0.Variable<int>(groupBy),
|
i0.Variable<int>(groupBy),
|
||||||
for (var $ in var2) i0.Variable<String>($)
|
for (var $ in var2) i0.Variable<String>($)
|
||||||
@@ -61,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
readsFrom: {
|
readsFrom: {
|
||||||
remoteAssetEntity,
|
remoteAssetEntity,
|
||||||
localAssetEntity,
|
localAssetEntity,
|
||||||
|
stackEntity,
|
||||||
}).map((i0.QueryRow row) => MergedBucketResult(
|
}).map((i0.QueryRow row) => MergedBucketResult(
|
||||||
assetCount: row.read<int>('asset_count'),
|
assetCount: row.read<int>('asset_count'),
|
||||||
bucketDate: row.read<String>('bucket_date'),
|
bucketDate: row.read<String>('bucket_date'),
|
||||||
@@ -73,6 +79,9 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
i4.$LocalAssetEntityTable get localAssetEntity =>
|
i4.$LocalAssetEntityTable get localAssetEntity =>
|
||||||
i1.ReadDatabaseContainer(attachedDatabase)
|
i1.ReadDatabaseContainer(attachedDatabase)
|
||||||
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
|
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
|
||||||
|
i5.$StackEntityTable get stackEntity =>
|
||||||
|
i1.ReadDatabaseContainer(attachedDatabase)
|
||||||
|
.resultSet<i5.$StackEntityTable>('stack_entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
class MergedAssetResult {
|
class MergedAssetResult {
|
||||||
@@ -91,6 +100,8 @@ class MergedAssetResult {
|
|||||||
final String? ownerId;
|
final String? ownerId;
|
||||||
final String? livePhotoVideoId;
|
final String? livePhotoVideoId;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
|
final String? stackId;
|
||||||
|
final int stackCount;
|
||||||
MergedAssetResult({
|
MergedAssetResult({
|
||||||
this.remoteId,
|
this.remoteId,
|
||||||
this.localId,
|
this.localId,
|
||||||
@@ -107,6 +118,8 @@ class MergedAssetResult {
|
|||||||
this.ownerId,
|
this.ownerId,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
|
this.stackId,
|
||||||
|
required this.stackCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class PersonEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const PersonEntity();
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
TextColumn get ownerId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get name => text()();
|
||||||
|
|
||||||
|
// TODO: foreign key refering to asset faces
|
||||||
|
TextColumn get faceAssetId => text().nullable()();
|
||||||
|
|
||||||
|
TextColumn get thumbnailPath => text()();
|
||||||
|
|
||||||
|
BoolColumn get isFavorite => boolean()();
|
||||||
|
|
||||||
|
BoolColumn get isHidden => boolean()();
|
||||||
|
|
||||||
|
TextColumn get color => text().nullable()();
|
||||||
|
|
||||||
|
DateTimeColumn get birthDate => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
@@ -0,0 +1,933 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/person.entity.dart' as i2;
|
||||||
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
|
as i4;
|
||||||
|
import 'package:drift/internal/modular.dart' as i5;
|
||||||
|
|
||||||
|
typedef $$PersonEntityTableCreateCompanionBuilder = i1.PersonEntityCompanion
|
||||||
|
Function({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
required String ownerId,
|
||||||
|
required String name,
|
||||||
|
i0.Value<String?> faceAssetId,
|
||||||
|
required String thumbnailPath,
|
||||||
|
required bool isFavorite,
|
||||||
|
required bool isHidden,
|
||||||
|
i0.Value<String?> color,
|
||||||
|
i0.Value<DateTime?> birthDate,
|
||||||
|
});
|
||||||
|
typedef $$PersonEntityTableUpdateCompanionBuilder = i1.PersonEntityCompanion
|
||||||
|
Function({
|
||||||
|
i0.Value<String> id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
i0.Value<String> ownerId,
|
||||||
|
i0.Value<String> name,
|
||||||
|
i0.Value<String?> faceAssetId,
|
||||||
|
i0.Value<String> thumbnailPath,
|
||||||
|
i0.Value<bool> isFavorite,
|
||||||
|
i0.Value<bool> isHidden,
|
||||||
|
i0.Value<String?> color,
|
||||||
|
i0.Value<DateTime?> birthDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final class $$PersonEntityTableReferences extends i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase, i1.$PersonEntityTable, i1.PersonEntityData> {
|
||||||
|
$$PersonEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
|
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$PersonEntityTable>('person_entity')
|
||||||
|
.ownerId,
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableProcessedTableManager get ownerId {
|
||||||
|
final $_column = $_itemColumn<String>('owner_id')!;
|
||||||
|
|
||||||
|
final manager = i4
|
||||||
|
.$$UserEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i5.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$PersonEntityTableFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||||
|
$$PersonEntityTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get name => $composableBuilder(
|
||||||
|
column: $table.name, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get faceAssetId => $composableBuilder(
|
||||||
|
column: $table.faceAssetId,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get thumbnailPath => $composableBuilder(
|
||||||
|
column: $table.thumbnailPath,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isHidden => $composableBuilder(
|
||||||
|
column: $table.isHidden, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get color => $composableBuilder(
|
||||||
|
column: $table.color, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get birthDate => $composableBuilder(
|
||||||
|
column: $table.birthDate, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$PersonEntityTableOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||||
|
$$PersonEntityTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get name => $composableBuilder(
|
||||||
|
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get faceAssetId => $composableBuilder(
|
||||||
|
column: $table.faceAssetId,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get thumbnailPath => $composableBuilder(
|
||||||
|
column: $table.thumbnailPath,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isHidden => $composableBuilder(
|
||||||
|
column: $table.isHidden, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get color => $composableBuilder(
|
||||||
|
column: $table.color, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get birthDate => $composableBuilder(
|
||||||
|
column: $table.birthDate,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$PersonEntityTableAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$PersonEntityTable> {
|
||||||
|
$$PersonEntityTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.GeneratedColumn<String> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get name =>
|
||||||
|
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get faceAssetId => $composableBuilder(
|
||||||
|
column: $table.faceAssetId, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get thumbnailPath => $composableBuilder(
|
||||||
|
column: $table.thumbnailPath, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
|
||||||
|
column: $table.isFavorite, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isHidden =>
|
||||||
|
$composableBuilder(column: $table.isHidden, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get color =>
|
||||||
|
$composableBuilder(column: $table.color, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get birthDate =>
|
||||||
|
$composableBuilder(column: $table.birthDate, builder: (column) => column);
|
||||||
|
|
||||||
|
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$PersonEntityTableTableManager extends i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$PersonEntityTable,
|
||||||
|
i1.PersonEntityData,
|
||||||
|
i1.$$PersonEntityTableFilterComposer,
|
||||||
|
i1.$$PersonEntityTableOrderingComposer,
|
||||||
|
i1.$$PersonEntityTableAnnotationComposer,
|
||||||
|
$$PersonEntityTableCreateCompanionBuilder,
|
||||||
|
$$PersonEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.PersonEntityData, i1.$$PersonEntityTableReferences),
|
||||||
|
i1.PersonEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId})> {
|
||||||
|
$$PersonEntityTableTableManager(
|
||||||
|
i0.GeneratedDatabase db, i1.$PersonEntityTable table)
|
||||||
|
: super(i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$$PersonEntityTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$$PersonEntityTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$$PersonEntityTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
i0.Value<String> id = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
i0.Value<String> ownerId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> name = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> thumbnailPath = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isHidden = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> color = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> birthDate = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.PersonEntityCompanion(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
name: name,
|
||||||
|
faceAssetId: faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
isHidden: isHidden,
|
||||||
|
color: color,
|
||||||
|
birthDate: birthDate,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String name,
|
||||||
|
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||||
|
required String thumbnailPath,
|
||||||
|
required bool isFavorite,
|
||||||
|
required bool isHidden,
|
||||||
|
i0.Value<String?> color = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> birthDate = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.PersonEntityCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
name: name,
|
||||||
|
faceAssetId: faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
isHidden: isHidden,
|
||||||
|
color: color,
|
||||||
|
birthDate: birthDate,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$PersonEntityTableReferences(db, table, e)
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: ({ownerId = false}) {
|
||||||
|
return i0.PrefetchHooks(
|
||||||
|
db: db,
|
||||||
|
explicitlyWatchedTables: [],
|
||||||
|
addJoins: <
|
||||||
|
T extends i0.TableManagerState<
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic>>(state) {
|
||||||
|
if (ownerId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.ownerId,
|
||||||
|
referencedTable:
|
||||||
|
i1.$$PersonEntityTableReferences._ownerIdTable(db),
|
||||||
|
referencedColumn:
|
||||||
|
i1.$$PersonEntityTableReferences._ownerIdTable(db).id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$PersonEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$PersonEntityTable,
|
||||||
|
i1.PersonEntityData,
|
||||||
|
i1.$$PersonEntityTableFilterComposer,
|
||||||
|
i1.$$PersonEntityTableOrderingComposer,
|
||||||
|
i1.$$PersonEntityTableAnnotationComposer,
|
||||||
|
$$PersonEntityTableCreateCompanionBuilder,
|
||||||
|
$$PersonEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.PersonEntityData, i1.$$PersonEntityTableReferences),
|
||||||
|
i1.PersonEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId})>;
|
||||||
|
|
||||||
|
class $PersonEntityTable extends i2.PersonEntity
|
||||||
|
with i0.TableInfo<$PersonEntityTable, i1.PersonEntityData> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$PersonEntityTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _createdAtMeta =
|
||||||
|
const i0.VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> createdAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i3.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _updatedAtMeta =
|
||||||
|
const i0.VerificationMeta('updatedAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i3.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _ownerIdMeta =
|
||||||
|
const i0.VerificationMeta('ownerId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
||||||
|
'owner_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
static const i0.VerificationMeta _nameMeta =
|
||||||
|
const i0.VerificationMeta('name');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
||||||
|
'name', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _faceAssetIdMeta =
|
||||||
|
const i0.VerificationMeta('faceAssetId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> faceAssetId =
|
||||||
|
i0.GeneratedColumn<String>('face_asset_id', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _thumbnailPathMeta =
|
||||||
|
const i0.VerificationMeta('thumbnailPath');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> thumbnailPath =
|
||||||
|
i0.GeneratedColumn<String>('thumbnail_path', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _isFavoriteMeta =
|
||||||
|
const i0.VerificationMeta('isFavorite');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isFavorite = i0.GeneratedColumn<bool>(
|
||||||
|
'is_favorite', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_favorite" IN (0, 1))'));
|
||||||
|
static const i0.VerificationMeta _isHiddenMeta =
|
||||||
|
const i0.VerificationMeta('isHidden');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isHidden = i0.GeneratedColumn<bool>(
|
||||||
|
'is_hidden', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_hidden" IN (0, 1))'));
|
||||||
|
static const i0.VerificationMeta _colorMeta =
|
||||||
|
const i0.VerificationMeta('color');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> color = i0.GeneratedColumn<String>(
|
||||||
|
'color', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _birthDateMeta =
|
||||||
|
const i0.VerificationMeta('birthDate');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> birthDate =
|
||||||
|
i0.GeneratedColumn<DateTime>('birth_date', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
ownerId,
|
||||||
|
name,
|
||||||
|
faceAssetId,
|
||||||
|
thumbnailPath,
|
||||||
|
isFavorite,
|
||||||
|
isHidden,
|
||||||
|
color,
|
||||||
|
birthDate
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'person_entity';
|
||||||
|
@override
|
||||||
|
i0.VerificationContext validateIntegrity(
|
||||||
|
i0.Insertable<i1.PersonEntityData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = i0.VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_idMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('updated_at')) {
|
||||||
|
context.handle(_updatedAtMeta,
|
||||||
|
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('owner_id')) {
|
||||||
|
context.handle(_ownerIdMeta,
|
||||||
|
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_ownerIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('name')) {
|
||||||
|
context.handle(
|
||||||
|
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_nameMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('face_asset_id')) {
|
||||||
|
context.handle(
|
||||||
|
_faceAssetIdMeta,
|
||||||
|
faceAssetId.isAcceptableOrUnknown(
|
||||||
|
data['face_asset_id']!, _faceAssetIdMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('thumbnail_path')) {
|
||||||
|
context.handle(
|
||||||
|
_thumbnailPathMeta,
|
||||||
|
thumbnailPath.isAcceptableOrUnknown(
|
||||||
|
data['thumbnail_path']!, _thumbnailPathMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_thumbnailPathMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('is_favorite')) {
|
||||||
|
context.handle(
|
||||||
|
_isFavoriteMeta,
|
||||||
|
isFavorite.isAcceptableOrUnknown(
|
||||||
|
data['is_favorite']!, _isFavoriteMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_isFavoriteMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('is_hidden')) {
|
||||||
|
context.handle(_isHiddenMeta,
|
||||||
|
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_isHiddenMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('color')) {
|
||||||
|
context.handle(
|
||||||
|
_colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('birth_date')) {
|
||||||
|
context.handle(_birthDateMeta,
|
||||||
|
birthDate.isAcceptableOrUnknown(data['birth_date']!, _birthDateMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
i1.PersonEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.PersonEntityData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
updatedAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
||||||
|
ownerId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
||||||
|
name: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||||
|
faceAssetId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string, data['${effectivePrefix}face_asset_id']),
|
||||||
|
thumbnailPath: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_path'])!,
|
||||||
|
isFavorite: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
|
||||||
|
isHidden: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_hidden'])!,
|
||||||
|
color: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}color']),
|
||||||
|
birthDate: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}birth_date']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$PersonEntityTable createAlias(String alias) {
|
||||||
|
return $PersonEntityTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get withoutRowId => true;
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonEntityData extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.PersonEntityData> {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String name;
|
||||||
|
final String? faceAssetId;
|
||||||
|
final String thumbnailPath;
|
||||||
|
final bool isFavorite;
|
||||||
|
final bool isHidden;
|
||||||
|
final String? color;
|
||||||
|
final DateTime? birthDate;
|
||||||
|
const PersonEntityData(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.name,
|
||||||
|
this.faceAssetId,
|
||||||
|
required this.thumbnailPath,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.isHidden,
|
||||||
|
this.color,
|
||||||
|
this.birthDate});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['id'] = i0.Variable<String>(id);
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||||
|
map['name'] = i0.Variable<String>(name);
|
||||||
|
if (!nullToAbsent || faceAssetId != null) {
|
||||||
|
map['face_asset_id'] = i0.Variable<String>(faceAssetId);
|
||||||
|
}
|
||||||
|
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath);
|
||||||
|
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||||
|
map['is_hidden'] = i0.Variable<bool>(isHidden);
|
||||||
|
if (!nullToAbsent || color != null) {
|
||||||
|
map['color'] = i0.Variable<String>(color);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || birthDate != null) {
|
||||||
|
map['birth_date'] = i0.Variable<DateTime>(birthDate);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PersonEntityData.fromJson(Map<String, dynamic> json,
|
||||||
|
{i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return PersonEntityData(
|
||||||
|
id: serializer.fromJson<String>(json['id']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
|
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||||
|
name: serializer.fromJson<String>(json['name']),
|
||||||
|
faceAssetId: serializer.fromJson<String?>(json['faceAssetId']),
|
||||||
|
thumbnailPath: serializer.fromJson<String>(json['thumbnailPath']),
|
||||||
|
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||||
|
isHidden: serializer.fromJson<bool>(json['isHidden']),
|
||||||
|
color: serializer.fromJson<String?>(json['color']),
|
||||||
|
birthDate: serializer.fromJson<DateTime?>(json['birthDate']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<String>(id),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||||
|
'ownerId': serializer.toJson<String>(ownerId),
|
||||||
|
'name': serializer.toJson<String>(name),
|
||||||
|
'faceAssetId': serializer.toJson<String?>(faceAssetId),
|
||||||
|
'thumbnailPath': serializer.toJson<String>(thumbnailPath),
|
||||||
|
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||||
|
'isHidden': serializer.toJson<bool>(isHidden),
|
||||||
|
'color': serializer.toJson<String?>(color),
|
||||||
|
'birthDate': serializer.toJson<DateTime?>(birthDate),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.PersonEntityData copyWith(
|
||||||
|
{String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? name,
|
||||||
|
i0.Value<String?> faceAssetId = const i0.Value.absent(),
|
||||||
|
String? thumbnailPath,
|
||||||
|
bool? isFavorite,
|
||||||
|
bool? isHidden,
|
||||||
|
i0.Value<String?> color = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> birthDate = const i0.Value.absent()}) =>
|
||||||
|
i1.PersonEntityData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
name: name ?? this.name,
|
||||||
|
faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
color: color.present ? color.value : this.color,
|
||||||
|
birthDate: birthDate.present ? birthDate.value : this.birthDate,
|
||||||
|
);
|
||||||
|
PersonEntityData copyWithCompanion(i1.PersonEntityCompanion data) {
|
||||||
|
return PersonEntityData(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||||
|
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||||
|
name: data.name.present ? data.name.value : this.name,
|
||||||
|
faceAssetId:
|
||||||
|
data.faceAssetId.present ? data.faceAssetId.value : this.faceAssetId,
|
||||||
|
thumbnailPath: data.thumbnailPath.present
|
||||||
|
? data.thumbnailPath.value
|
||||||
|
: this.thumbnailPath,
|
||||||
|
isFavorite:
|
||||||
|
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
|
||||||
|
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
|
||||||
|
color: data.color.present ? data.color.value : this.color,
|
||||||
|
birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('PersonEntityData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('faceAssetId: $faceAssetId, ')
|
||||||
|
..write('thumbnailPath: $thumbnailPath, ')
|
||||||
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('isHidden: $isHidden, ')
|
||||||
|
..write('color: $color, ')
|
||||||
|
..write('birthDate: $birthDate')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, createdAt, updatedAt, ownerId, name,
|
||||||
|
faceAssetId, thumbnailPath, isFavorite, isHidden, color, birthDate);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.PersonEntityData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.updatedAt == this.updatedAt &&
|
||||||
|
other.ownerId == this.ownerId &&
|
||||||
|
other.name == this.name &&
|
||||||
|
other.faceAssetId == this.faceAssetId &&
|
||||||
|
other.thumbnailPath == this.thumbnailPath &&
|
||||||
|
other.isFavorite == this.isFavorite &&
|
||||||
|
other.isHidden == this.isHidden &&
|
||||||
|
other.color == this.color &&
|
||||||
|
other.birthDate == this.birthDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonEntityCompanion extends i0.UpdateCompanion<i1.PersonEntityData> {
|
||||||
|
final i0.Value<String> id;
|
||||||
|
final i0.Value<DateTime> createdAt;
|
||||||
|
final i0.Value<DateTime> updatedAt;
|
||||||
|
final i0.Value<String> ownerId;
|
||||||
|
final i0.Value<String> name;
|
||||||
|
final i0.Value<String?> faceAssetId;
|
||||||
|
final i0.Value<String> thumbnailPath;
|
||||||
|
final i0.Value<bool> isFavorite;
|
||||||
|
final i0.Value<bool> isHidden;
|
||||||
|
final i0.Value<String?> color;
|
||||||
|
final i0.Value<DateTime?> birthDate;
|
||||||
|
const PersonEntityCompanion({
|
||||||
|
this.id = const i0.Value.absent(),
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
this.ownerId = const i0.Value.absent(),
|
||||||
|
this.name = const i0.Value.absent(),
|
||||||
|
this.faceAssetId = const i0.Value.absent(),
|
||||||
|
this.thumbnailPath = const i0.Value.absent(),
|
||||||
|
this.isFavorite = const i0.Value.absent(),
|
||||||
|
this.isHidden = const i0.Value.absent(),
|
||||||
|
this.color = const i0.Value.absent(),
|
||||||
|
this.birthDate = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
PersonEntityCompanion.insert({
|
||||||
|
required String id,
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String name,
|
||||||
|
this.faceAssetId = const i0.Value.absent(),
|
||||||
|
required String thumbnailPath,
|
||||||
|
required bool isFavorite,
|
||||||
|
required bool isHidden,
|
||||||
|
this.color = const i0.Value.absent(),
|
||||||
|
this.birthDate = const i0.Value.absent(),
|
||||||
|
}) : id = i0.Value(id),
|
||||||
|
ownerId = i0.Value(ownerId),
|
||||||
|
name = i0.Value(name),
|
||||||
|
thumbnailPath = i0.Value(thumbnailPath),
|
||||||
|
isFavorite = i0.Value(isFavorite),
|
||||||
|
isHidden = i0.Value(isHidden);
|
||||||
|
static i0.Insertable<i1.PersonEntityData> custom({
|
||||||
|
i0.Expression<String>? id,
|
||||||
|
i0.Expression<DateTime>? createdAt,
|
||||||
|
i0.Expression<DateTime>? updatedAt,
|
||||||
|
i0.Expression<String>? ownerId,
|
||||||
|
i0.Expression<String>? name,
|
||||||
|
i0.Expression<String>? faceAssetId,
|
||||||
|
i0.Expression<String>? thumbnailPath,
|
||||||
|
i0.Expression<bool>? isFavorite,
|
||||||
|
i0.Expression<bool>? isHidden,
|
||||||
|
i0.Expression<String>? color,
|
||||||
|
i0.Expression<DateTime>? birthDate,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
|
if (ownerId != null) 'owner_id': ownerId,
|
||||||
|
if (name != null) 'name': name,
|
||||||
|
if (faceAssetId != null) 'face_asset_id': faceAssetId,
|
||||||
|
if (thumbnailPath != null) 'thumbnail_path': thumbnailPath,
|
||||||
|
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||||
|
if (isHidden != null) 'is_hidden': isHidden,
|
||||||
|
if (color != null) 'color': color,
|
||||||
|
if (birthDate != null) 'birth_date': birthDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.PersonEntityCompanion copyWith(
|
||||||
|
{i0.Value<String>? id,
|
||||||
|
i0.Value<DateTime>? createdAt,
|
||||||
|
i0.Value<DateTime>? updatedAt,
|
||||||
|
i0.Value<String>? ownerId,
|
||||||
|
i0.Value<String>? name,
|
||||||
|
i0.Value<String?>? faceAssetId,
|
||||||
|
i0.Value<String>? thumbnailPath,
|
||||||
|
i0.Value<bool>? isFavorite,
|
||||||
|
i0.Value<bool>? isHidden,
|
||||||
|
i0.Value<String?>? color,
|
||||||
|
i0.Value<DateTime?>? birthDate}) {
|
||||||
|
return i1.PersonEntityCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
name: name ?? this.name,
|
||||||
|
faceAssetId: faceAssetId ?? this.faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath ?? this.thumbnailPath,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
color: color ?? this.color,
|
||||||
|
birthDate: birthDate ?? this.birthDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = i0.Variable<String>(id.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (updatedAt.present) {
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||||
|
}
|
||||||
|
if (ownerId.present) {
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||||
|
}
|
||||||
|
if (name.present) {
|
||||||
|
map['name'] = i0.Variable<String>(name.value);
|
||||||
|
}
|
||||||
|
if (faceAssetId.present) {
|
||||||
|
map['face_asset_id'] = i0.Variable<String>(faceAssetId.value);
|
||||||
|
}
|
||||||
|
if (thumbnailPath.present) {
|
||||||
|
map['thumbnail_path'] = i0.Variable<String>(thumbnailPath.value);
|
||||||
|
}
|
||||||
|
if (isFavorite.present) {
|
||||||
|
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
|
||||||
|
}
|
||||||
|
if (isHidden.present) {
|
||||||
|
map['is_hidden'] = i0.Variable<bool>(isHidden.value);
|
||||||
|
}
|
||||||
|
if (color.present) {
|
||||||
|
map['color'] = i0.Variable<String>(color.value);
|
||||||
|
}
|
||||||
|
if (birthDate.present) {
|
||||||
|
map['birth_date'] = i0.Variable<DateTime>(birthDate.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('PersonEntityCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('faceAssetId: $faceAssetId, ')
|
||||||
|
..write('thumbnailPath: $thumbnailPath, ')
|
||||||
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('isHidden: $isHidden, ')
|
||||||
|
..write('color: $color, ')
|
||||||
|
..write('birthDate: $birthDate')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ class RemoteAssetEntity extends Table
|
|||||||
|
|
||||||
IntColumn get visibility => intEnum<AssetVisibility>()();
|
IntColumn get visibility => intEnum<AssetVisibility>()();
|
||||||
|
|
||||||
|
TextColumn get stackId => text().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
@@ -55,5 +57,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
localId: null,
|
localId: null,
|
||||||
|
stackId: stackId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
|
|||||||
i0.Value<DateTime?> deletedAt,
|
i0.Value<DateTime?> deletedAt,
|
||||||
i0.Value<String?> livePhotoVideoId,
|
i0.Value<String?> livePhotoVideoId,
|
||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
|
i0.Value<String?> stackId,
|
||||||
});
|
});
|
||||||
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
||||||
= i1.RemoteAssetEntityCompanion Function({
|
= i1.RemoteAssetEntityCompanion Function({
|
||||||
@@ -48,6 +49,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
|||||||
i0.Value<DateTime?> deletedAt,
|
i0.Value<DateTime?> deletedAt,
|
||||||
i0.Value<String?> livePhotoVideoId,
|
i0.Value<String?> livePhotoVideoId,
|
||||||
i0.Value<i2.AssetVisibility> visibility,
|
i0.Value<i2.AssetVisibility> visibility,
|
||||||
|
i0.Value<String?> stackId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences<
|
final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences<
|
||||||
@@ -145,6 +147,9 @@ class $$RemoteAssetEntityTableFilterComposer
|
|||||||
column: $table.visibility,
|
column: $table.visibility,
|
||||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get stackId => $composableBuilder(
|
||||||
|
column: $table.stackId, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -231,6 +236,9 @@ class $$RemoteAssetEntityTableOrderingComposer
|
|||||||
column: $table.visibility,
|
column: $table.visibility,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get stackId => $composableBuilder(
|
||||||
|
column: $table.stackId, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -309,6 +317,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
|
|||||||
$composableBuilder(
|
$composableBuilder(
|
||||||
column: $table.visibility, builder: (column) => column);
|
column: $table.visibility, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get stackId =>
|
||||||
|
$composableBuilder(column: $table.stackId, builder: (column) => column);
|
||||||
|
|
||||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -373,6 +384,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||||
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
|
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
i1.RemoteAssetEntityCompanion(
|
i1.RemoteAssetEntityCompanion(
|
||||||
name: name,
|
name: name,
|
||||||
@@ -391,6 +403,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
stackId: stackId,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -409,6 +422,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
i1.RemoteAssetEntityCompanion.insert(
|
i1.RemoteAssetEntityCompanion.insert(
|
||||||
name: name,
|
name: name,
|
||||||
@@ -427,6 +441,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
stackId: stackId,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
@@ -602,6 +617,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
||||||
.withConverter<i2.AssetVisibility>(
|
.withConverter<i2.AssetVisibility>(
|
||||||
i1.$RemoteAssetEntityTable.$convertervisibility);
|
i1.$RemoteAssetEntityTable.$convertervisibility);
|
||||||
|
static const i0.VerificationMeta _stackIdMeta =
|
||||||
|
const i0.VerificationMeta('stackId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> stackId = i0.GeneratedColumn<String>(
|
||||||
|
'stack_id', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
name,
|
name,
|
||||||
@@ -619,7 +640,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
livePhotoVideoId,
|
livePhotoVideoId,
|
||||||
visibility
|
visibility,
|
||||||
|
stackId
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -703,6 +725,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
livePhotoVideoId.isAcceptableOrUnknown(
|
livePhotoVideoId.isAcceptableOrUnknown(
|
||||||
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
|
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('stack_id')) {
|
||||||
|
context.handle(_stackIdMeta,
|
||||||
|
stackId.isAcceptableOrUnknown(data['stack_id']!, _stackIdMeta));
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,6 +774,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
|
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
|
||||||
attachedDatabase.typeMapping.read(
|
attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
|
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
|
||||||
|
stackId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}stack_id']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +813,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
final String? livePhotoVideoId;
|
final String? livePhotoVideoId;
|
||||||
final i2.AssetVisibility visibility;
|
final i2.AssetVisibility visibility;
|
||||||
|
final String? stackId;
|
||||||
const RemoteAssetEntityData(
|
const RemoteAssetEntityData(
|
||||||
{required this.name,
|
{required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -801,7 +830,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
required this.visibility});
|
required this.visibility,
|
||||||
|
this.stackId});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, i0.Expression>{};
|
final map = <String, i0.Expression>{};
|
||||||
@@ -841,6 +871,9 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
map['visibility'] = i0.Variable<int>(
|
map['visibility'] = i0.Variable<int>(
|
||||||
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
|
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || stackId != null) {
|
||||||
|
map['stack_id'] = i0.Variable<String>(stackId);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,6 +899,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
|
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
|
||||||
visibility: i1.$RemoteAssetEntityTable.$convertervisibility
|
visibility: i1.$RemoteAssetEntityTable.$convertervisibility
|
||||||
.fromJson(serializer.fromJson<int>(json['visibility'])),
|
.fromJson(serializer.fromJson<int>(json['visibility'])),
|
||||||
|
stackId: serializer.fromJson<String?>(json['stackId']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -890,6 +924,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
|
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
|
||||||
'visibility': serializer.toJson<int>(
|
'visibility': serializer.toJson<int>(
|
||||||
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
|
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
|
||||||
|
'stackId': serializer.toJson<String?>(stackId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,7 +944,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
i0.Value<String?> thumbHash = const i0.Value.absent(),
|
i0.Value<String?> thumbHash = const i0.Value.absent(),
|
||||||
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
|
||||||
i2.AssetVisibility? visibility}) =>
|
i2.AssetVisibility? visibility,
|
||||||
|
i0.Value<String?> stackId = const i0.Value.absent()}) =>
|
||||||
i1.RemoteAssetEntityData(
|
i1.RemoteAssetEntityData(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -932,6 +968,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
? livePhotoVideoId.value
|
? livePhotoVideoId.value
|
||||||
: this.livePhotoVideoId,
|
: this.livePhotoVideoId,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
|
stackId: stackId.present ? stackId.value : this.stackId,
|
||||||
);
|
);
|
||||||
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
|
||||||
return RemoteAssetEntityData(
|
return RemoteAssetEntityData(
|
||||||
@@ -959,6 +996,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
: this.livePhotoVideoId,
|
: this.livePhotoVideoId,
|
||||||
visibility:
|
visibility:
|
||||||
data.visibility.present ? data.visibility.value : this.visibility,
|
data.visibility.present ? data.visibility.value : this.visibility,
|
||||||
|
stackId: data.stackId.present ? data.stackId.value : this.stackId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,7 +1018,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
..write('thumbHash: $thumbHash, ')
|
..write('thumbHash: $thumbHash, ')
|
||||||
..write('deletedAt: $deletedAt, ')
|
..write('deletedAt: $deletedAt, ')
|
||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility')
|
..write('visibility: $visibility, ')
|
||||||
|
..write('stackId: $stackId')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@@ -1002,7 +1041,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
livePhotoVideoId,
|
livePhotoVideoId,
|
||||||
visibility);
|
visibility,
|
||||||
|
stackId);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -1022,7 +1062,8 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
other.thumbHash == this.thumbHash &&
|
other.thumbHash == this.thumbHash &&
|
||||||
other.deletedAt == this.deletedAt &&
|
other.deletedAt == this.deletedAt &&
|
||||||
other.livePhotoVideoId == this.livePhotoVideoId &&
|
other.livePhotoVideoId == this.livePhotoVideoId &&
|
||||||
other.visibility == this.visibility);
|
other.visibility == this.visibility &&
|
||||||
|
other.stackId == this.stackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteAssetEntityCompanion
|
class RemoteAssetEntityCompanion
|
||||||
@@ -1043,6 +1084,7 @@ class RemoteAssetEntityCompanion
|
|||||||
final i0.Value<DateTime?> deletedAt;
|
final i0.Value<DateTime?> deletedAt;
|
||||||
final i0.Value<String?> livePhotoVideoId;
|
final i0.Value<String?> livePhotoVideoId;
|
||||||
final i0.Value<i2.AssetVisibility> visibility;
|
final i0.Value<i2.AssetVisibility> visibility;
|
||||||
|
final i0.Value<String?> stackId;
|
||||||
const RemoteAssetEntityCompanion({
|
const RemoteAssetEntityCompanion({
|
||||||
this.name = const i0.Value.absent(),
|
this.name = const i0.Value.absent(),
|
||||||
this.type = const i0.Value.absent(),
|
this.type = const i0.Value.absent(),
|
||||||
@@ -1060,6 +1102,7 @@ class RemoteAssetEntityCompanion
|
|||||||
this.deletedAt = const i0.Value.absent(),
|
this.deletedAt = const i0.Value.absent(),
|
||||||
this.livePhotoVideoId = const i0.Value.absent(),
|
this.livePhotoVideoId = const i0.Value.absent(),
|
||||||
this.visibility = const i0.Value.absent(),
|
this.visibility = const i0.Value.absent(),
|
||||||
|
this.stackId = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
RemoteAssetEntityCompanion.insert({
|
RemoteAssetEntityCompanion.insert({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -1078,6 +1121,7 @@ class RemoteAssetEntityCompanion
|
|||||||
this.deletedAt = const i0.Value.absent(),
|
this.deletedAt = const i0.Value.absent(),
|
||||||
this.livePhotoVideoId = const i0.Value.absent(),
|
this.livePhotoVideoId = const i0.Value.absent(),
|
||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
|
this.stackId = const i0.Value.absent(),
|
||||||
}) : name = i0.Value(name),
|
}) : name = i0.Value(name),
|
||||||
type = i0.Value(type),
|
type = i0.Value(type),
|
||||||
id = i0.Value(id),
|
id = i0.Value(id),
|
||||||
@@ -1101,6 +1145,7 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Expression<DateTime>? deletedAt,
|
i0.Expression<DateTime>? deletedAt,
|
||||||
i0.Expression<String>? livePhotoVideoId,
|
i0.Expression<String>? livePhotoVideoId,
|
||||||
i0.Expression<int>? visibility,
|
i0.Expression<int>? visibility,
|
||||||
|
i0.Expression<String>? stackId,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (name != null) 'name': name,
|
if (name != null) 'name': name,
|
||||||
@@ -1119,6 +1164,7 @@ class RemoteAssetEntityCompanion
|
|||||||
if (deletedAt != null) 'deleted_at': deletedAt,
|
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||||
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
|
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
|
||||||
if (visibility != null) 'visibility': visibility,
|
if (visibility != null) 'visibility': visibility,
|
||||||
|
if (stackId != null) 'stack_id': stackId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,7 +1184,8 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Value<String?>? thumbHash,
|
i0.Value<String?>? thumbHash,
|
||||||
i0.Value<DateTime?>? deletedAt,
|
i0.Value<DateTime?>? deletedAt,
|
||||||
i0.Value<String?>? livePhotoVideoId,
|
i0.Value<String?>? livePhotoVideoId,
|
||||||
i0.Value<i2.AssetVisibility>? visibility}) {
|
i0.Value<i2.AssetVisibility>? visibility,
|
||||||
|
i0.Value<String?>? stackId}) {
|
||||||
return i1.RemoteAssetEntityCompanion(
|
return i1.RemoteAssetEntityCompanion(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -1156,6 +1203,7 @@ class RemoteAssetEntityCompanion
|
|||||||
deletedAt: deletedAt ?? this.deletedAt,
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
|
stackId: stackId ?? this.stackId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,6 +1261,9 @@ class RemoteAssetEntityCompanion
|
|||||||
.$RemoteAssetEntityTable.$convertervisibility
|
.$RemoteAssetEntityTable.$convertervisibility
|
||||||
.toSql(visibility.value));
|
.toSql(visibility.value));
|
||||||
}
|
}
|
||||||
|
if (stackId.present) {
|
||||||
|
map['stack_id'] = i0.Variable<String>(stackId.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,7 +1285,8 @@ class RemoteAssetEntityCompanion
|
|||||||
..write('thumbHash: $thumbHash, ')
|
..write('thumbHash: $thumbHash, ')
|
||||||
..write('deletedAt: $deletedAt, ')
|
..write('deletedAt: $deletedAt, ')
|
||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility')
|
..write('visibility: $visibility, ')
|
||||||
|
..write('stackId: $stackId')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:drift/drift.dart';
|
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/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ class StackEntity extends Table with DriftDefaultsMixin {
|
|||||||
TextColumn get ownerId =>
|
TextColumn get ownerId =>
|
||||||
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
|
TextColumn get primaryAssetId => text()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
|
|||||||
+16
-119
@@ -8,8 +8,6 @@ import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
|||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
as i4;
|
as i4;
|
||||||
import 'package:drift/internal/modular.dart' as i5;
|
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
|
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
|
||||||
Function({
|
Function({
|
||||||
@@ -57,33 +55,6 @@ final class $$StackEntityTableReferences extends i0.BaseReferences<
|
|||||||
return i0.ProcessedTableManager(
|
return i0.ProcessedTableManager(
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
}
|
}
|
||||||
|
|
||||||
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
|
|
||||||
i0.GeneratedDatabase db) =>
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i1.$StackEntityTable>('stack_entity')
|
|
||||||
.primaryAssetId,
|
|
||||||
i5.ReadDatabaseContainer(db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
|
||||||
.id));
|
|
||||||
|
|
||||||
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
|
|
||||||
final $_column = $_itemColumn<String>('primary_asset_id')!;
|
|
||||||
|
|
||||||
final manager = i6
|
|
||||||
.$$RemoteAssetEntityTableTableManager(
|
|
||||||
$_db,
|
|
||||||
i5.ReadDatabaseContainer($_db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
|
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
|
||||||
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
|
|
||||||
if (item == null) return manager;
|
|
||||||
return i0.ProcessedTableManager(
|
|
||||||
manager.$state.copyWith(prefetchedData: [item]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$StackEntityTableFilterComposer
|
class $$StackEntityTableFilterComposer
|
||||||
@@ -104,6 +75,10 @@ class $$StackEntityTableFilterComposer
|
|||||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||||
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get primaryAssetId => $composableBuilder(
|
||||||
|
column: $table.primaryAssetId,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i4.$$UserEntityTableFilterComposer get ownerId {
|
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||||
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -125,28 +100,6 @@ class $$StackEntityTableFilterComposer
|
|||||||
));
|
));
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
|
|
||||||
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.primaryAssetId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$RemoteAssetEntityTableFilterComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$StackEntityTableOrderingComposer
|
class $$StackEntityTableOrderingComposer
|
||||||
@@ -169,6 +122,10 @@ class $$StackEntityTableOrderingComposer
|
|||||||
column: $table.updatedAt,
|
column: $table.updatedAt,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get primaryAssetId => $composableBuilder(
|
||||||
|
column: $table.primaryAssetId,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i4.$$UserEntityTableOrderingComposer get ownerId {
|
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -190,30 +147,6 @@ class $$StackEntityTableOrderingComposer
|
|||||||
));
|
));
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
|
|
||||||
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.primaryAssetId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$RemoteAssetEntityTableOrderingComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$StackEntityTableAnnotationComposer
|
class $$StackEntityTableAnnotationComposer
|
||||||
@@ -234,6 +167,9 @@ class $$StackEntityTableAnnotationComposer
|
|||||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get primaryAssetId => $composableBuilder(
|
||||||
|
column: $table.primaryAssetId, builder: (column) => column);
|
||||||
|
|
||||||
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -255,30 +191,6 @@ class $$StackEntityTableAnnotationComposer
|
|||||||
));
|
));
|
||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
|
|
||||||
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
|
|
||||||
$composerBuilder(
|
|
||||||
composer: this,
|
|
||||||
getCurrentColumn: (t) => t.primaryAssetId,
|
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
|
||||||
getReferencedColumn: (t) => t.id,
|
|
||||||
builder: (joinBuilder,
|
|
||||||
{$addJoinBuilderToRootComposer,
|
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
|
||||||
i6.$$RemoteAssetEntityTableAnnotationComposer(
|
|
||||||
$db: $db,
|
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
|
||||||
.resultSet<i6.$RemoteAssetEntityTable>(
|
|
||||||
'remote_asset_entity'),
|
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
|
||||||
joinBuilder: joinBuilder,
|
|
||||||
$removeJoinBuilderFromRootComposer:
|
|
||||||
$removeJoinBuilderFromRootComposer,
|
|
||||||
));
|
|
||||||
return composer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$StackEntityTableTableManager extends i0.RootTableManager<
|
class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||||
@@ -292,7 +204,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
|||||||
$$StackEntityTableUpdateCompanionBuilder,
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
i1.StackEntityData,
|
i1.StackEntityData,
|
||||||
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
|
i0.PrefetchHooks Function({bool ownerId})> {
|
||||||
$$StackEntityTableTableManager(
|
$$StackEntityTableTableManager(
|
||||||
i0.GeneratedDatabase db, i1.$StackEntityTable table)
|
i0.GeneratedDatabase db, i1.$StackEntityTable table)
|
||||||
: super(i0.TableManagerState(
|
: super(i0.TableManagerState(
|
||||||
@@ -338,7 +250,7 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i1.$$StackEntityTableReferences(db, table, e)
|
i1.$$StackEntityTableReferences(db, table, e)
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) {
|
prefetchHooksCallback: ({ownerId = false}) {
|
||||||
return i0.PrefetchHooks(
|
return i0.PrefetchHooks(
|
||||||
db: db,
|
db: db,
|
||||||
explicitlyWatchedTables: [],
|
explicitlyWatchedTables: [],
|
||||||
@@ -365,17 +277,6 @@ class $$StackEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
|
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
|
||||||
) as T;
|
) 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;
|
return state;
|
||||||
},
|
},
|
||||||
@@ -398,7 +299,7 @@ typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
|||||||
$$StackEntityTableUpdateCompanionBuilder,
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
i1.StackEntityData,
|
i1.StackEntityData,
|
||||||
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
|
i0.PrefetchHooks Function({bool ownerId})>;
|
||||||
|
|
||||||
class $StackEntityTable extends i2.StackEntity
|
class $StackEntityTable extends i2.StackEntity
|
||||||
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
|
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
|
||||||
@@ -440,12 +341,8 @@ class $StackEntityTable extends i2.StackEntity
|
|||||||
const i0.VerificationMeta('primaryAssetId');
|
const i0.VerificationMeta('primaryAssetId');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<String> primaryAssetId =
|
late final i0.GeneratedColumn<String> primaryAssetId =
|
||||||
i0.GeneratedColumn<String>(
|
i0.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
|
||||||
'primary_asset_id', aliasedName, false,
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
type: i0.DriftSqlType.string,
|
|
||||||
requiredDuringInsert: true,
|
|
||||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
|
||||||
'REFERENCES remote_asset_entity (id)'));
|
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns =>
|
List<i0.GeneratedColumn> get $columns =>
|
||||||
[id, createdAt, updatedAt, ownerId, primaryAssetId];
|
[id, createdAt, updatedAt, ownerId, primaryAssetId];
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import "package:immich_mobile/utils/database.utils.dart";
|
||||||
|
|
||||||
|
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
||||||
|
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class DriftBackupRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftBackupRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
_getExcludedSubquery() {
|
||||||
|
return _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equalsValue(BackupSelection.excluded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getTotalCount() async {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equalsValue(BackupSelection.selected) &
|
||||||
|
_db.localAlbumAssetEntity.assetId
|
||||||
|
.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getRemainderCount() async {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equalsValue(BackupSelection.selected) &
|
||||||
|
_db.remoteAssetEntity.id.isNull() &
|
||||||
|
_db.localAlbumAssetEntity.assetId
|
||||||
|
.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getBackupCount() async {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns(
|
||||||
|
[_db.localAlbumAssetEntity.assetId],
|
||||||
|
)
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equalsValue(BackupSelection.selected) &
|
||||||
|
_db.remoteAssetEntity.id.isNotNull() &
|
||||||
|
_db.localAlbumAssetEntity.assetId
|
||||||
|
.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalAsset>> getCandidates() async {
|
||||||
|
final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns([_db.localAlbumEntity.id])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equalsValue(BackupSelection.selected),
|
||||||
|
);
|
||||||
|
|
||||||
|
final query = _db.localAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(lae) =>
|
||||||
|
existsQuery(
|
||||||
|
_db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumAssetEntity.albumId
|
||||||
|
.isInQuery(selectedAlbumIds) &
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
|
||||||
|
),
|
||||||
|
) &
|
||||||
|
notExistsQuery(
|
||||||
|
_db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.remoteAssetEntity.checksum])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) &
|
||||||
|
lae.checksum.isNotNull(),
|
||||||
|
),
|
||||||
|
) &
|
||||||
|
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
)
|
||||||
|
..orderBy(
|
||||||
|
[
|
||||||
|
(localAsset) => OrderingTerm.desc(localAsset.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((localAsset) => localAsset.toDto()).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_flutter/drift_flutter.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/domain/interfaces/db.interface.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.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.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||||
@@ -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/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.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/entities/user_metadata.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import 'db.repository.drift.dart';
|
import 'db.repository.drift.dart';
|
||||||
@@ -52,6 +55,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
MemoryEntity,
|
MemoryEntity,
|
||||||
MemoryAssetEntity,
|
MemoryAssetEntity,
|
||||||
StackEntity,
|
StackEntity,
|
||||||
|
PersonEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
@@ -68,10 +72,40 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
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 {
|
beforeOpen: (details) async {
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
await customStatement('PRAGMA synchronous = NORMAL');
|
await customStatement('PRAGMA synchronous = NORMAL');
|
||||||
|
|||||||
+79
-66
@@ -7,31 +7,33 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
|||||||
as i2;
|
as i2;
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||||
as i3;
|
as i3;
|
||||||
import 'package:immich_mobile/infrastructure/entities/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'
|
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;
|
as i14;
|
||||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||||
as i15;
|
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 {
|
abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
$Drift(i0.QueryExecutor e) : super(e);
|
$Drift(i0.QueryExecutor e) : super(e);
|
||||||
@@ -41,28 +43,29 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i2.$RemoteAssetEntityTable(this);
|
i2.$RemoteAssetEntityTable(this);
|
||||||
late final i3.$LocalAssetEntityTable localAssetEntity =
|
late final i3.$LocalAssetEntityTable localAssetEntity =
|
||||||
i3.$LocalAssetEntityTable(this);
|
i3.$LocalAssetEntityTable(this);
|
||||||
late final i4.$UserMetadataEntityTable userMetadataEntity =
|
late final i4.$StackEntityTable stackEntity = i4.$StackEntityTable(this);
|
||||||
i4.$UserMetadataEntityTable(this);
|
late final i5.$UserMetadataEntityTable userMetadataEntity =
|
||||||
late final i5.$PartnerEntityTable partnerEntity =
|
i5.$UserMetadataEntityTable(this);
|
||||||
i5.$PartnerEntityTable(this);
|
late final i6.$PartnerEntityTable partnerEntity =
|
||||||
late final i6.$LocalAlbumEntityTable localAlbumEntity =
|
i6.$PartnerEntityTable(this);
|
||||||
i6.$LocalAlbumEntityTable(this);
|
late final i7.$LocalAlbumEntityTable localAlbumEntity =
|
||||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
i7.$LocalAlbumEntityTable(this);
|
||||||
i7.$LocalAlbumAssetEntityTable(this);
|
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||||
late final i8.$RemoteExifEntityTable remoteExifEntity =
|
i8.$LocalAlbumAssetEntityTable(this);
|
||||||
i8.$RemoteExifEntityTable(this);
|
late final i9.$RemoteExifEntityTable remoteExifEntity =
|
||||||
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity =
|
i9.$RemoteExifEntityTable(this);
|
||||||
i9.$RemoteAlbumEntityTable(this);
|
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity =
|
||||||
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
|
i10.$RemoteAlbumEntityTable(this);
|
||||||
i10.$RemoteAlbumAssetEntityTable(this);
|
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
|
||||||
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
|
i11.$RemoteAlbumAssetEntityTable(this);
|
||||||
i11.$RemoteAlbumUserEntityTable(this);
|
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
|
||||||
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
|
i12.$RemoteAlbumUserEntityTable(this);
|
||||||
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
|
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
|
||||||
i13.$MemoryAssetEntityTable(this);
|
late final i14.$MemoryAssetEntityTable memoryAssetEntity =
|
||||||
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
|
i14.$MemoryAssetEntityTable(this);
|
||||||
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
|
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
|
||||||
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
|
i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this)
|
||||||
|
.accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
|
||||||
@override
|
@override
|
||||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
@@ -71,6 +74,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
userEntity,
|
userEntity,
|
||||||
remoteAssetEntity,
|
remoteAssetEntity,
|
||||||
localAssetEntity,
|
localAssetEntity,
|
||||||
|
stackEntity,
|
||||||
i3.idxLocalAssetChecksum,
|
i3.idxLocalAssetChecksum,
|
||||||
i2.uQRemoteAssetOwnerChecksum,
|
i2.uQRemoteAssetOwnerChecksum,
|
||||||
i2.idxRemoteAssetChecksum,
|
i2.idxRemoteAssetChecksum,
|
||||||
@@ -84,7 +88,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
remoteAlbumUserEntity,
|
remoteAlbumUserEntity,
|
||||||
memoryEntity,
|
memoryEntity,
|
||||||
memoryAssetEntity,
|
memoryAssetEntity,
|
||||||
stackEntity
|
personEntity
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||||
@@ -97,6 +101,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
i0.WritePropagation(
|
i0.WritePropagation(
|
||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
@@ -213,7 +224,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
limitUpdateKind: i0.UpdateKind.delete),
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
result: [
|
result: [
|
||||||
i0.TableUpdate('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);
|
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||||
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||||
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||||
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
i4.$$StackEntityTableTableManager get stackEntity =>
|
||||||
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
i4.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||||
i5.$$PartnerEntityTableTableManager get partnerEntity =>
|
i5.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||||
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
i6.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||||
|
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||||
|
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
|
||||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||||
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||||
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||||
i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||||
i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||||
i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||||
i10.$$RemoteAlbumAssetEntityTableTableManager(
|
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
||||||
_db, _db.remoteAlbumAssetEntity);
|
_db, _db.remoteAlbumAssetEntity);
|
||||||
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
|
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
|
||||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||||
i12.$$MemoryEntityTableTableManager get memoryEntity =>
|
i13.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||||
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||||
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||||
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||||
i14.$$StackEntityTableTableManager get stackEntity =>
|
i15.$$PersonEntityTableTableManager get personEntity =>
|
||||||
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,16 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d
|
|||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_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/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/database.utils.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum }
|
enum SortLocalAlbumsBy {
|
||||||
|
id,
|
||||||
|
backupSelection,
|
||||||
|
isIosSharedAlbum,
|
||||||
|
name,
|
||||||
|
assetCount
|
||||||
|
}
|
||||||
|
|
||||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -40,6 +47,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
OrderingTerm.asc(_db.localAlbumEntity.backupSelection),
|
OrderingTerm.asc(_db.localAlbumEntity.backupSelection),
|
||||||
SortLocalAlbumsBy.isIosSharedAlbum =>
|
SortLocalAlbumsBy.isIosSharedAlbum =>
|
||||||
OrderingTerm.asc(_db.localAlbumEntity.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(
|
batch.insert(
|
||||||
_db.localAlbumEntity,
|
_db.localAlbumEntity,
|
||||||
companion,
|
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;
|
return results.isNotEmpty ? results.first : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on LocalAlbumEntityData {
|
|
||||||
LocalAlbum toDto({int assetCount = 0}) {
|
|
||||||
return LocalAlbum(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
assetCount: assetCount,
|
|
||||||
backupSelection: backupSelection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on LocalAssetEntityData {
|
|
||||||
LocalAsset toDto() {
|
|
||||||
return LocalAsset(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
checksum: checksum,
|
|
||||||
type: type,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
durationInSeconds: durationInSeconds,
|
|
||||||
isFavorite: isFavorite,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
@@ -43,4 +44,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> ids) {
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _db.batch((batch) {
|
||||||
|
for (final slice in ids.slices(32000)) {
|
||||||
|
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> getById(String id) {
|
||||||
|
final query = _db.localAssetEntity.select()
|
||||||
|
..where((lae) => lae.id.equals(id));
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftPartnerRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftPartnerRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getPartners(String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedWithId.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who we can share our library with
|
||||||
|
Future<List<PartnerUserDto>> getAvailablePartners(String currentUserId) {
|
||||||
|
final query = _db.select(_db.userEntity)
|
||||||
|
..where((row) => row.id.equals(currentUserId).not());
|
||||||
|
|
||||||
|
return query.map((user) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: false,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who are sharing their photos WITH the current user
|
||||||
|
Future<List<PartnerUserDto>> getSharedWith(String partnerId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedWithId.equals(partnerId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who the current user is sharing their photos TO
|
||||||
|
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedById.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getAllPartnerIds(String userId) async {
|
||||||
|
// Get users who are sharing with me (sharedWithId = userId)
|
||||||
|
final sharingWithMeQuery = _db.select(_db.partnerEntity)
|
||||||
|
..where((tbl) => tbl.sharedWithId.equals(userId));
|
||||||
|
final sharingWithMe =
|
||||||
|
await sharingWithMeQuery.map((row) => row.sharedById).get();
|
||||||
|
|
||||||
|
// Get users who I am sharing with (sharedById = userId)
|
||||||
|
final sharingWithThemQuery = _db.select(_db.partnerEntity)
|
||||||
|
..where((tbl) => tbl.sharedById.equals(userId));
|
||||||
|
final sharingWithThem =
|
||||||
|
await sharingWithThemQuery.map((row) => row.sharedWithId).get();
|
||||||
|
|
||||||
|
// Combine both lists and remove duplicates
|
||||||
|
final allPartnerIds =
|
||||||
|
<String>{...sharingWithMe, ...sharingWithThem}.toList();
|
||||||
|
return allPartnerIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PartnerUserDto?> getPartner(String partnerId, String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedById.equals(partnerId) &
|
||||||
|
_db.partnerEntity.sharedWithId.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> toggleShowInTimeline(PartnerUserDto partner, String userId) {
|
||||||
|
return _db.partnerEntity.update().replace(
|
||||||
|
PartnerEntityCompanion(
|
||||||
|
sharedById: Value(partner.id),
|
||||||
|
sharedWithId: Value(userId),
|
||||||
|
inTimeline: Value(!partner.inTimeline),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> create(String partnerId, String userId) {
|
||||||
|
final entity = PartnerEntityCompanion(
|
||||||
|
sharedById: Value(userId),
|
||||||
|
sharedWithId: Value(partnerId),
|
||||||
|
inTimeline: const Value(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _db.partnerEntity.insertOne(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(String partnerId, String userId) {
|
||||||
|
return _db.partnerEntity.deleteWhere(
|
||||||
|
(t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftPersonRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftPersonRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<Person>> getAll(String userId) {
|
||||||
|
final query = _db.personEntity.select()
|
||||||
|
..where((e) => e.ownerId.equals(userId));
|
||||||
|
|
||||||
|
return query.map((person) {
|
||||||
|
return person.toDto();
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on PersonEntityData {
|
||||||
|
Person toDto() {
|
||||||
|
return Person(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
name: name,
|
||||||
|
faceAssetId: faceAssetId,
|
||||||
|
thumbnailPath: thumbnailPath,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
isHidden: isHidden,
|
||||||
|
color: color,
|
||||||
|
birthDate: birthDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.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.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
enum SortRemoteAlbumsBy { id, updatedAt }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
@@ -99,11 +105,169 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> update(RemoteAlbum album) async {
|
||||||
|
await _db.remoteAlbumEntity.update().replace(
|
||||||
|
RemoteAlbumEntityCompanion(
|
||||||
|
id: Value(album.id),
|
||||||
|
name: Value(album.name),
|
||||||
|
ownerId: Value(album.ownerId),
|
||||||
|
createdAt: Value(album.createdAt),
|
||||||
|
updatedAt: Value(album.updatedAt),
|
||||||
|
description: Value(album.description),
|
||||||
|
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||||
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
|
order: Value(album.order),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> removeAssets(String albumId, List<String> assetIds) {
|
Future<int> removeAssets(String albumId, List<String> assetIds) {
|
||||||
return _db.remoteAlbumAssetEntity.deleteWhere(
|
return _db.remoteAlbumAssetEntity.deleteWhere(
|
||||||
(tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds),
|
(tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||||
|
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||||
|
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..addColumns([
|
||||||
|
_db.remoteAssetEntity.createdAt.min(),
|
||||||
|
_db.remoteAssetEntity.createdAt.max(),
|
||||||
|
])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.remoteAssetEntity.id
|
||||||
|
.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
|
||||||
|
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
|
||||||
|
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
|
||||||
|
}).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<UserDto>> getSharedUsers(String albumId) async {
|
||||||
|
final albumUserRows = await (_db.select(_db.remoteAlbumUserEntity)
|
||||||
|
..where((row) => row.albumId.equals(albumId)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (albumUserRows.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final userIds = albumUserRows.map((row) => row.userId);
|
||||||
|
|
||||||
|
return (_db.select(_db.userEntity)..where((row) => row.id.isIn(userIds)))
|
||||||
|
.map(
|
||||||
|
(user) => UserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
profileImagePath: user.profileImagePath?.isEmpty == true
|
||||||
|
? null
|
||||||
|
: user.profileImagePath,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
quotaSizeInBytes: user.quotaSizeInBytes ?? 0,
|
||||||
|
quotaUsageInBytes: user.quotaUsageInBytes,
|
||||||
|
memoryEnabled: true,
|
||||||
|
inTimeline: false,
|
||||||
|
isPartnerSharedBy: false,
|
||||||
|
isPartnerSharedWith: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
|
final query = _db.remoteAlbumAssetEntity.select().join([
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId));
|
||||||
|
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
||||||
|
final albumAssets = assetIds.map(
|
||||||
|
(assetId) => RemoteAlbumAssetEntityCompanion(
|
||||||
|
albumId: Value(albumId),
|
||||||
|
assetId: Value(assetId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
albumAssets,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return assetIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||||
|
final albumUsers = userIds.map(
|
||||||
|
(assetId) => RemoteAlbumUserEntityCompanion(
|
||||||
|
albumId: Value(albumId),
|
||||||
|
userId: Value(assetId),
|
||||||
|
role: const Value(AlbumUserRole.editor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _db.batch((batch) {
|
||||||
|
batch.insertAll(
|
||||||
|
_db.remoteAlbumUserEntity,
|
||||||
|
albumUsers,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _db.remoteAlbumEntity.deleteWhere(
|
||||||
|
(table) => table.id.equals(albumId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
|
final query = _db.remoteAlbumEntity.select().join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
||||||
|
..addColumns([_db.userEntity.name])
|
||||||
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final album = row.readTable(_db.remoteAlbumEntity).toDto(
|
||||||
|
ownerName: row.read(_db.userEntity.name)!,
|
||||||
|
);
|
||||||
|
return album;
|
||||||
|
}).watchSingleOrNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||||
hide ExifInfo;
|
hide ExifInfo;
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@@ -30,25 +32,66 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Stream<RemoteAsset?> watchAsset(String id) {
|
Stream<RemoteAsset?> watchAsset(String id) {
|
||||||
final query = _db.remoteAssetEntity
|
final stackCountRef = _db.stackEntity.id.count();
|
||||||
.select()
|
|
||||||
.addColumns([_db.localAssetEntity.id]).join([
|
final query = _db.remoteAssetEntity.select().addColumns([
|
||||||
|
_db.localAssetEntity.id,
|
||||||
|
_db.stackEntity.primaryAssetId,
|
||||||
|
stackCountRef,
|
||||||
|
]).join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
_db.localAssetEntity,
|
_db.localAssetEntity,
|
||||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.stackEntity,
|
||||||
|
_db.stackEntity.primaryAssetId.equalsExp(_db.remoteAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity.createAlias('stacked_assets'),
|
||||||
|
_db.stackEntity.id.equalsExp(
|
||||||
|
_db.remoteAssetEntity.createAlias('stacked_assets').stackId,
|
||||||
|
),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
..where(_db.remoteAssetEntity.id.equals(id));
|
..where(_db.remoteAssetEntity.id.equals(id))
|
||||||
|
..groupBy([
|
||||||
|
_db.remoteAssetEntity.id,
|
||||||
|
_db.localAssetEntity.id,
|
||||||
|
_db.stackEntity.primaryAssetId,
|
||||||
|
]);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
||||||
|
final primaryAssetId = row.read(_db.stackEntity.primaryAssetId);
|
||||||
|
final stackCount =
|
||||||
|
primaryAssetId == id ? (row.read(stackCountRef) ?? 0) : 0;
|
||||||
|
|
||||||
return asset.copyWith(
|
return asset.copyWith(
|
||||||
localId: row.read(_db.localAssetEntity.id),
|
localId: row.read(_db.localAssetEntity.id),
|
||||||
|
stackCount: stackCount,
|
||||||
);
|
);
|
||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||||
|
if (asset.stackId == null) {
|
||||||
|
return Future.value([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not(),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<ExifInfo?> getExif(String id) {
|
Future<ExifInfo?> getExif(String id) {
|
||||||
return _db.managers.remoteExifEntity
|
return _db.managers.remoteExifEntity
|
||||||
.filter((row) => row.assetId.id.equals(id))
|
.filter((row) => row.assetId.id.equals(id))
|
||||||
@@ -146,4 +189,53 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> stack(String userId, StackResponse stack) {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
final stackIds = await _db.managers.stackEntity
|
||||||
|
.filter((row) => row.primaryAssetId.isIn(stack.assetIds))
|
||||||
|
.map((row) => row.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
final companion = StackEntityCompanion(
|
||||||
|
ownerId: Value(userId),
|
||||||
|
primaryAssetId: Value(stack.primaryAssetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.stackEntity,
|
||||||
|
companion.copyWith(id: Value(stack.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final assetId in stack.assetIds) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(
|
||||||
|
stackId: Value(stack.id),
|
||||||
|
),
|
||||||
|
where: (e) => e.id.equals(assetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unStack(List<String> stackIds) {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||||
|
|
||||||
|
// TODO: delete this after adding foreign key on stackId
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
||||||
|
where: (e) => e.stackId.isIn(stackIds),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'
|
||||||
|
hide AssetVisibility;
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class SearchApiRepository extends ApiRepository {
|
||||||
|
final SearchApi _api;
|
||||||
|
const SearchApiRepository(this._api);
|
||||||
|
|
||||||
|
Future<SearchResponseDto?> search(SearchFilter filter, int page) {
|
||||||
|
AssetTypeEnum? type;
|
||||||
|
if (filter.mediaType.index == AssetType.image.index) {
|
||||||
|
type = AssetTypeEnum.IMAGE;
|
||||||
|
} else if (filter.mediaType.index == AssetType.video.index) {
|
||||||
|
type = AssetTypeEnum.VIDEO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.context != null && filter.context!.isNotEmpty) {
|
||||||
|
return _api.searchSmart(
|
||||||
|
SmartSearchDto(
|
||||||
|
query: filter.context!,
|
||||||
|
language: filter.language,
|
||||||
|
country: filter.location.country,
|
||||||
|
state: filter.location.state,
|
||||||
|
city: filter.location.city,
|
||||||
|
make: filter.camera.make,
|
||||||
|
model: filter.camera.model,
|
||||||
|
takenAfter: filter.date.takenAfter,
|
||||||
|
takenBefore: filter.date.takenBefore,
|
||||||
|
visibility: filter.display.isArchive
|
||||||
|
? AssetVisibility.archive
|
||||||
|
: AssetVisibility.timeline,
|
||||||
|
isFavorite: filter.display.isFavorite ? true : null,
|
||||||
|
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||||
|
personIds: filter.people.map((e) => e.id).toList(),
|
||||||
|
type: type,
|
||||||
|
page: page,
|
||||||
|
size: 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _api.searchAssets(
|
||||||
|
MetadataSearchDto(
|
||||||
|
originalFileName: filter.filename != null && filter.filename!.isNotEmpty
|
||||||
|
? filter.filename
|
||||||
|
: null,
|
||||||
|
country: filter.location.country,
|
||||||
|
description:
|
||||||
|
filter.description != null && filter.description!.isNotEmpty
|
||||||
|
? filter.description
|
||||||
|
: null,
|
||||||
|
state: filter.location.state,
|
||||||
|
city: filter.location.city,
|
||||||
|
make: filter.camera.make,
|
||||||
|
model: filter.camera.model,
|
||||||
|
takenAfter: filter.date.takenAfter,
|
||||||
|
takenBefore: filter.date.takenBefore,
|
||||||
|
visibility: filter.display.isArchive
|
||||||
|
? AssetVisibility.archive
|
||||||
|
: AssetVisibility.timeline,
|
||||||
|
isFavorite: filter.display.isFavorite ? true : null,
|
||||||
|
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||||
|
personIds: filter.people.map((e) => e.id).toList(),
|
||||||
|
type: type,
|
||||||
|
page: page,
|
||||||
|
size: 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>?> getSearchSuggestions(
|
||||||
|
SearchSuggestionType type, {
|
||||||
|
String? country,
|
||||||
|
String? state,
|
||||||
|
String? make,
|
||||||
|
String? model,
|
||||||
|
}) =>
|
||||||
|
_api.getSearchSuggestions(
|
||||||
|
type,
|
||||||
|
country: country,
|
||||||
|
state: state,
|
||||||
|
make: make,
|
||||||
|
model: model,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
@@ -7,8 +8,9 @@ class StorageRepository {
|
|||||||
const StorageRepository();
|
const StorageRepository();
|
||||||
|
|
||||||
Future<File?> getFileForAsset(String assetId) async {
|
Future<File?> getFileForAsset(String assetId) async {
|
||||||
final log = Logger('StorageRepository');
|
|
||||||
File? file;
|
File? file;
|
||||||
|
final log = Logger('StorageRepository');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final entity = await AssetEntity.fromId(assetId);
|
final entity = await AssetEntity.fromId(assetId);
|
||||||
file = await entity?.originFile;
|
file = await entity?.originFile;
|
||||||
@@ -20,4 +22,48 @@ class StorageRepository {
|
|||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
||||||
|
File? file;
|
||||||
|
final log = Logger('StorageRepository');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final entity = await AssetEntity.fromId(asset.id);
|
||||||
|
file = await entity?.originFileWithSubtype;
|
||||||
|
if (file == null) {
|
||||||
|
log.warning(
|
||||||
|
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
log.warning(
|
||||||
|
"Error getting motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset) async {
|
||||||
|
final log = Logger('StorageRepository');
|
||||||
|
|
||||||
|
AssetEntity? entity;
|
||||||
|
|
||||||
|
try {
|
||||||
|
entity = await AssetEntity.fromId(asset.id);
|
||||||
|
if (entity == null) {
|
||||||
|
log.warning(
|
||||||
|
"Cannot get AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
log.warning(
|
||||||
|
"Error getting AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.stacksV1,
|
SyncRequestType.stacksV1,
|
||||||
SyncRequestType.partnerStacksV1,
|
SyncRequestType.partnerStacksV1,
|
||||||
SyncRequestType.userMetadataV1,
|
SyncRequestType.userMetadataV1,
|
||||||
|
SyncRequestType.peopleV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@@ -173,6 +174,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||||
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
|
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
|
||||||
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
|
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
|
||||||
|
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
||||||
|
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
class _SyncAckV1 {
|
class _SyncAckV1 {
|
||||||
|
|||||||
@@ -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.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
@@ -137,6 +138,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
deletedAt: Value(asset.deletedAt),
|
deletedAt: Value(asset.deletedAt),
|
||||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||||
|
stackId: Value(asset.stackId),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(
|
batch.insert(
|
||||||
@@ -510,6 +512,47 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updatePeopleV1(Iterable<SyncPersonV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final person in data) {
|
||||||
|
final companion = PersonEntityCompanion(
|
||||||
|
createdAt: Value(person.createdAt),
|
||||||
|
updatedAt: Value(person.updatedAt),
|
||||||
|
ownerId: Value(person.ownerId),
|
||||||
|
name: Value(person.name),
|
||||||
|
faceAssetId: Value(person.faceAssetId),
|
||||||
|
isFavorite: Value(person.isFavorite),
|
||||||
|
isHidden: Value(person.isHidden),
|
||||||
|
color: Value(person.color),
|
||||||
|
birthDate: Value(person.birthDate),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.personEntity,
|
||||||
|
companion.copyWith(id: Value(person.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updatePeopleV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePeopleV1(
|
||||||
|
Iterable<SyncPersonDeleteV1> data,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _db.personEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.personId)),
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deletePeopleV1', error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AssetTypeEnum {
|
extension on AssetTypeEnum {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:immich_mobile/constants/constants.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
@@ -88,6 +89,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
isFavorite: row.isFavorite,
|
isFavorite: row.isFavorite,
|
||||||
durationInSeconds: row.durationInSeconds,
|
durationInSeconds: row.durationInSeconds,
|
||||||
livePhotoVideoId: row.livePhotoVideoId,
|
livePhotoVideoId: row.livePhotoVideoId,
|
||||||
|
stackId: row.stackId,
|
||||||
|
stackCount: row.stackCount,
|
||||||
)
|
)
|
||||||
: LocalAsset(
|
: LocalAsset(
|
||||||
id: row.localId!,
|
id: row.localId!,
|
||||||
@@ -164,15 +167,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
..addColumns([_db.remoteAssetEntity.id])
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query
|
return query.map((row) {
|
||||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||||
.get();
|
return asset.copyWith(
|
||||||
|
remoteId: row.read(_db.remoteAssetEntity.id),
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
|
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||||
@@ -195,41 +208,75 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return _db.remoteAlbumAssetEntity
|
return _db.remoteAlbumAssetEntity
|
||||||
.count(where: (row) => row.albumId.equals(albumId))
|
.count(where: (row) => row.albumId.equals(albumId))
|
||||||
.map(_generateBuckets)
|
.map(_generateBuckets)
|
||||||
.watchSingle();
|
.watch()
|
||||||
|
.map((results) => results.isNotEmpty ? results.first : <Bucket>[])
|
||||||
|
.handleError((error) {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
return (_db.remoteAlbumEntity.select()
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
..where((row) => row.id.equals(albumId)))
|
||||||
|
.watch()
|
||||||
|
.switchMap((albums) {
|
||||||
|
if (albums.isEmpty) {
|
||||||
|
return Stream.value(<Bucket>[]);
|
||||||
|
}
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
final album = albums.first;
|
||||||
..addColumns([assetCountExp, dateExp])
|
final isAscending = album.order == AlbumAssetOrder.asc;
|
||||||
..join([
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
innerJoin(
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
_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)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
..addColumns([assetCountExp, dateExp])
|
||||||
final assetCount = row.read(assetCountExp)!;
|
..join([
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
innerJoin(
|
||||||
}).watch();
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.assetId
|
||||||
|
.equalsExp(_db.remoteAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
|
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp]);
|
||||||
|
|
||||||
|
if (isAscending) {
|
||||||
|
query.orderBy([OrderingTerm.asc(dateExp)]);
|
||||||
|
} else {
|
||||||
|
query.orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}).handleError((error) {
|
||||||
|
// If there's an error (e.g., album was deleted), return empty buckets
|
||||||
|
return <Bucket>[];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(
|
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
}) {
|
}) async {
|
||||||
|
final albumData = await (_db.remoteAlbumEntity.select()
|
||||||
|
..where((row) => row.id.equals(albumId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
// If album doesn't exist (was deleted), return empty list
|
||||||
|
if (albumData == null) {
|
||||||
|
return <BaseAsset>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final isAscending = albumData.order == AlbumAssetOrder.asc;
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.select().join(
|
final query = _db.remoteAssetEntity.select().join(
|
||||||
[
|
[
|
||||||
innerJoin(
|
innerJoin(
|
||||||
@@ -239,18 +286,30 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)..where(
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
|
_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
|
return query
|
||||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimelineQuery fromAssets(List<BaseAsset> assets) => (
|
||||||
|
bucketSource: () => Stream.value(_generateBuckets(assets.length)),
|
||||||
|
assetSource: (offset, count) =>
|
||||||
|
Future.value(assets.skip(offset).take(count).toList()),
|
||||||
|
);
|
||||||
|
|
||||||
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
|
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
|
||||||
_remoteQueryBuilder(
|
_remoteQueryBuilder(
|
||||||
filter: (row) =>
|
filter: (row) =>
|
||||||
|
|||||||
+34
-1
@@ -31,6 +31,7 @@ import 'package:immich_mobile/utils/bootstrap.dart';
|
|||||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||||
import 'package:immich_mobile/utils/download.dart';
|
import 'package:immich_mobile/utils/download.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.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:immich_mobile/utils/migration.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -92,12 +93,30 @@ Future<void> initApp() async {
|
|||||||
|
|
||||||
initializeTimeZones();
|
initializeTimeZones();
|
||||||
|
|
||||||
|
// Initialize the file downloader
|
||||||
|
|
||||||
|
await FileDownloader().configure(
|
||||||
|
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||||
|
globalConfig: (Config.holdingQueue, (6, 6, 3)),
|
||||||
|
);
|
||||||
|
|
||||||
await FileDownloader().trackTasksInGroup(
|
await FileDownloader().trackTasksInGroup(
|
||||||
downloadGroupLivePhoto,
|
downloadGroupLivePhoto,
|
||||||
markDownloadedComplete: false,
|
markDownloadedComplete: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileDownloader().trackTasks();
|
await FileDownloader().trackTasks();
|
||||||
|
|
||||||
|
LicenseRegistry.addLicense(
|
||||||
|
() async* {
|
||||||
|
for (final license in nonPubLicenses.entries) {
|
||||||
|
yield LicenseEntryWithLineBreaks(
|
||||||
|
[license.key],
|
||||||
|
license.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
@@ -159,7 +178,21 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _configureFileDownloaderNotifications() {
|
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(
|
running: TaskNotification(
|
||||||
'downloading_media'.tr(),
|
'downloading_media'.tr(),
|
||||||
'${'file_name'.tr()}: {filename}',
|
'${'file_name'.tr()}: {filename}',
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class SearchFilter {
|
|||||||
String? filename;
|
String? filename;
|
||||||
String? description;
|
String? description;
|
||||||
String? language;
|
String? language;
|
||||||
Set<Person> people;
|
Set<PersonDto> people;
|
||||||
SearchLocationFilter location;
|
SearchLocationFilter location;
|
||||||
SearchCameraFilter camera;
|
SearchCameraFilter camera;
|
||||||
SearchDateFilter date;
|
SearchDateFilter date;
|
||||||
@@ -282,7 +282,7 @@ class SearchFilter {
|
|||||||
String? filename,
|
String? filename,
|
||||||
String? description,
|
String? description,
|
||||||
String? language,
|
String? language,
|
||||||
Set<Person>? people,
|
Set<PersonDto>? people,
|
||||||
SearchLocationFilter? location,
|
SearchLocationFilter? location,
|
||||||
SearchCameraFilter? camera,
|
SearchCameraFilter? camera,
|
||||||
SearchDateFilter? date,
|
SearchDateFilter? date,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ enum UploadStatus {
|
|||||||
notFound,
|
notFound,
|
||||||
failed,
|
failed,
|
||||||
canceled,
|
canceled,
|
||||||
waitingtoRetry,
|
waitingToRetry,
|
||||||
paused,
|
paused,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,40 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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';
|
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
|
||||||
class AlbumControlButton extends ConsumerWidget {
|
class AlbumControlButton extends ConsumerWidget {
|
||||||
void Function() onAddPhotosPressed;
|
final void Function()? onAddPhotosPressed;
|
||||||
void Function() onAddUsersPressed;
|
final void Function()? onAddUsersPressed;
|
||||||
|
|
||||||
AlbumControlButton({
|
const AlbumControlButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onAddPhotosPressed,
|
this.onAddPhotosPressed,
|
||||||
required this.onAddUsersPressed,
|
this.onAddUsersPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final userId = ref.watch(authProvider).userId;
|
return SizedBox(
|
||||||
final isOwner = ref.watch(
|
height: 36,
|
||||||
currentAlbumProvider.select((album) {
|
child: ListView(
|
||||||
return album?.ownerId == userId;
|
scrollDirection: Axis.horizontal,
|
||||||
}),
|
children: [
|
||||||
);
|
if (onAddPhotosPressed != null)
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 36,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: [
|
|
||||||
AlbumActionFilledButton(
|
AlbumActionFilledButton(
|
||||||
key: const ValueKey('add_photos_button'),
|
key: const ValueKey('add_photos_button'),
|
||||||
iconData: Icons.add_photo_alternate_outlined,
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
onPressed: onAddPhotosPressed,
|
onPressed: onAddPhotosPressed,
|
||||||
labelText: "add_photos".tr(),
|
labelText: "add_photos".tr(),
|
||||||
),
|
),
|
||||||
if (isOwner)
|
if (onAddUsersPressed != null)
|
||||||
AlbumActionFilledButton(
|
AlbumActionFilledButton(
|
||||||
key: const ValueKey('add_users_button'),
|
key: const ValueKey('add_users_button'),
|
||||||
iconData: Icons.person_add_alt_rounded,
|
iconData: Icons.person_add_alt_rounded,
|
||||||
onPressed: onAddUsersPressed,
|
onPressed: onAddUsersPressed,
|
||||||
labelText: "album_viewer_page_share_add_users".tr(),
|
labelText: "album_viewer_page_share_add_users".tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
final userId = ref.watch(authProvider).userId;
|
final userId = ref.watch(authProvider).userId;
|
||||||
final isMultiselecting = ref.watch(multiselectProvider);
|
final isMultiselecting = ref.watch(multiselectProvider);
|
||||||
final isProcessing = useProcessingOverlay();
|
final isProcessing = useProcessingOverlay();
|
||||||
|
final isOwner = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
return album?.ownerId == userId;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||||
final bool isSuccess =
|
final bool isSuccess =
|
||||||
@@ -138,10 +143,13 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const AlbumSharedUserIcons(),
|
const AlbumSharedUserIcons(),
|
||||||
if (album.isRemote)
|
if (album.isRemote)
|
||||||
AlbumControlButton(
|
Padding(
|
||||||
key: const ValueKey("albumControlButton"),
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
onAddPhotosPressed: onAddPhotosPressed,
|
child: AlbumControlButton(
|
||||||
onAddUsersPressed: onAddUsersPressed,
|
key: const ValueKey("albumControlButton"),
|
||||||
|
onAddPhotosPressed: onAddPhotosPressed,
|
||||||
|
onAddUsersPressed: isOwner ? onAddUsersPressed : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftBackupPage extends ConsumerStatefulWidget {
|
||||||
|
const DriftBackupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftBackupPage> createState() => _DriftBackupPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startBackup() async {
|
||||||
|
await ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||||
|
await ref.read(driftBackupProvider.notifier).backup();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopBackup() async {
|
||||||
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedAlbum = ref
|
||||||
|
.watch(backupAlbumProvider)
|
||||||
|
.where(
|
||||||
|
(album) => album.backupSelection == BackupSelection.selected,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final uploadItems = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.uploadItems),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(
|
||||||
|
"backup_controller_page_backup".t(),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.maybePop(true);
|
||||||
|
},
|
||||||
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
right: 16,
|
||||||
|
bottom: 32,
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const _BackupAlbumSelectionCard(),
|
||||||
|
if (selectedAlbum.isNotEmpty) ...[
|
||||||
|
const _TotalCard(),
|
||||||
|
const _BackupCard(),
|
||||||
|
const _RemainderCard(),
|
||||||
|
const Divider(),
|
||||||
|
BackupToggleButton(
|
||||||
|
onStart: () async => await startBackup(),
|
||||||
|
onStop: () async => await stopBackup(),
|
||||||
|
),
|
||||||
|
if (uploadItems.isNotEmpty)
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.info_outline_rounded),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
const DriftUploadDetailRoute(),
|
||||||
|
),
|
||||||
|
label: Text("view_details".t(context: context)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackupAlbumSelectionCard extends ConsumerWidget {
|
||||||
|
const _BackupAlbumSelectionCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Widget buildSelectedAlbumName() {
|
||||||
|
String text = "backup_controller_page_backup_selected".tr();
|
||||||
|
final albums = ref
|
||||||
|
.watch(backupAlbumProvider)
|
||||||
|
.where(
|
||||||
|
(album) => album.backupSelection == BackupSelection.selected,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
if (album.name == "Recent" || album.name == "Recents") {
|
||||||
|
text += "${album.name} (${'all'.tr()}), ";
|
||||||
|
} else {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"backup_controller_page_none_selected".tr(),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildExcludedAlbumName() {
|
||||||
|
String text = "backup_controller_page_excluded".tr();
|
||||||
|
final albums = ref
|
||||||
|
.watch(backupAlbumProvider)
|
||||||
|
.where(
|
||||||
|
(album) => album.backupSelection == BackupSelection.excluded,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: Colors.red[300],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: ListTile(
|
||||||
|
minVerticalPadding: 18,
|
||||||
|
title: Text(
|
||||||
|
"backup_controller_page_albums",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
).tr(),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"backup_controller_page_to_backup",
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
buildSelectedAlbumName(),
|
||||||
|
buildExcludedAlbumName(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await context.pushRoute(const DriftBackupAlbumSelectionRoute());
|
||||||
|
ref.read(driftBackupProvider.notifier).getBackupStatus();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"select",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TotalCard extends ConsumerWidget {
|
||||||
|
const _TotalCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final totalCount =
|
||||||
|
ref.watch(driftBackupProvider.select((p) => p.totalCount));
|
||||||
|
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "total".tr(),
|
||||||
|
subtitle: "backup_controller_page_total_sub".tr(),
|
||||||
|
info: totalCount.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackupCard extends ConsumerWidget {
|
||||||
|
const _BackupCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final backupCount =
|
||||||
|
ref.watch(driftBackupProvider.select((p) => p.backupCount));
|
||||||
|
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "backup_controller_page_backup".tr(),
|
||||||
|
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||||
|
info: backupCount.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemainderCard extends ConsumerWidget {
|
||||||
|
const _RemainderCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final remainderCount =
|
||||||
|
ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "backup_controller_page_remainder".tr(),
|
||||||
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
|
info: remainderCount.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||||
|
const DriftBackupAlbumSelectionPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftBackupAlbumSelectionPage> createState() =>
|
||||||
|
_DriftBackupAlbumSelectionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftBackupAlbumSelectionPageState
|
||||||
|
extends ConsumerState<DriftBackupAlbumSelectionPage> {
|
||||||
|
String _searchQuery = '';
|
||||||
|
bool _isSearchMode = false;
|
||||||
|
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||||
|
late TextEditingController _searchController;
|
||||||
|
late FocusNode _searchFocusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_enableSyncUploadAlbum = ValueNotifier<bool>(false);
|
||||||
|
_searchController = TextEditingController();
|
||||||
|
_searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
_enableSyncUploadAlbum.value = ref
|
||||||
|
.read(appSettingsServiceProvider)
|
||||||
|
.getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
ref.read(backupAlbumProvider.notifier).getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_enableSyncUploadAlbum.dispose();
|
||||||
|
_searchController.dispose();
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final albums = ref.watch(backupAlbumProvider);
|
||||||
|
final albumCount = albums.length;
|
||||||
|
// Filter albums based on search query
|
||||||
|
final filteredAlbums = albums.where((album) {
|
||||||
|
if (_searchQuery.isEmpty) return true;
|
||||||
|
return album.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final selectedBackupAlbums = albums
|
||||||
|
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||||
|
.toList();
|
||||||
|
final excludedBackupAlbums = albums
|
||||||
|
.where((album) => album.backupSelection == BackupSelection.excluded)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
handleSyncAlbumToggle(bool isEnable) async {
|
||||||
|
if (isEnable) {
|
||||||
|
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
|
for (final album in selectedBackupAlbums) {
|
||||||
|
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => context.maybePop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
title: _isSearchMode
|
||||||
|
? SearchField(
|
||||||
|
hintText: 'search_albums'.t(context: context),
|
||||||
|
autofocus: true,
|
||||||
|
controller: _searchController,
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
onChanged: (value) =>
|
||||||
|
setState(() => _searchQuery = value.trim()),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
"backup_album_selection_page_select_albums",
|
||||||
|
).t(context: context),
|
||||||
|
actions: [
|
||||||
|
if (!_isSearchMode)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_isSearchMode = true;
|
||||||
|
_searchQuery = '';
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_isSearchMode = false;
|
||||||
|
_searchQuery = '';
|
||||||
|
_searchController.clear();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"backup_album_selection_page_selection_info",
|
||||||
|
style: context.textTheme.titleSmall,
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
// Selected Album Chips
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
_SelectedAlbumNameChips(
|
||||||
|
selectedBackupAlbums: selectedBackupAlbums,
|
||||||
|
),
|
||||||
|
_ExcludedAlbumNameChips(
|
||||||
|
excludedBackupAlbums: excludedBackupAlbums,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: _enableSyncUploadAlbum,
|
||||||
|
title: "sync_albums".t(context: context),
|
||||||
|
subtitle:
|
||||||
|
"sync_upload_album_setting_subtitle".t(context: context),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
onChanged: handleSyncAlbumToggle,
|
||||||
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"albums_on_device_count".t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': albumCount.toString()},
|
||||||
|
),
|
||||||
|
style: context.textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"backup_album_selection_page_albums_tap",
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
splashRadius: 16,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 20,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// show the dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(10)),
|
||||||
|
),
|
||||||
|
elevation: 5,
|
||||||
|
title: Text(
|
||||||
|
'backup_album_selection_page_selection_info',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).t(context: context),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'backup_album_selection_page_assets_scatter',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
).t(context: context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
_SelectAllButton(
|
||||||
|
filteredAlbums: filteredAlbums,
|
||||||
|
selectedBackupAlbums: selectedBackupAlbums,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverLayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (constraints.crossAxisExtent > 600) {
|
||||||
|
return _AlbumSelectionGrid(
|
||||||
|
filteredAlbums: filteredAlbums,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _AlbumSelectionList(
|
||||||
|
filteredAlbums: filteredAlbums,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSelectionList extends StatelessWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final String searchQuery;
|
||||||
|
|
||||||
|
const _AlbumSelectionList({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Text('album_search_not_found'.t(context: context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredAlbums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
((context, index) {
|
||||||
|
return DriftAlbumInfoListTile(
|
||||||
|
album: filteredAlbums[index],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
childCount: filteredAlbums.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSelectionGrid extends StatelessWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final String searchQuery;
|
||||||
|
|
||||||
|
const _AlbumSelectionGrid({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (filteredAlbums.isEmpty && searchQuery.isNotEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Text('album_search_not_found'.t(context: context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredAlbums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
sliver: SliverGrid.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 300,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
),
|
||||||
|
itemCount: filteredAlbums.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return DriftAlbumInfoListTile(
|
||||||
|
album: filteredAlbums[index],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectedAlbumNameChips extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> selectedBackupAlbums;
|
||||||
|
|
||||||
|
const _SelectedAlbumNameChips({
|
||||||
|
required this.selectedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Wrap(
|
||||||
|
children: selectedBackupAlbums.asMap().entries.map((entry) {
|
||||||
|
final album = entry.value;
|
||||||
|
|
||||||
|
void removeSelection() {
|
||||||
|
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: context.primaryColor,
|
||||||
|
deleteIconColor:
|
||||||
|
context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExcludedAlbumNameChips extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> excludedBackupAlbums;
|
||||||
|
|
||||||
|
const _ExcludedAlbumNameChips({
|
||||||
|
required this.excludedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Wrap(
|
||||||
|
children: excludedBackupAlbums.asMap().entries.map((entry) {
|
||||||
|
final album = entry.value;
|
||||||
|
|
||||||
|
void removeSelection() {
|
||||||
|
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: context.scaffoldBackgroundColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red[300],
|
||||||
|
deleteIconColor: context.scaffoldBackgroundColor,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectAllButton extends ConsumerWidget {
|
||||||
|
final List<LocalAlbum> filteredAlbums;
|
||||||
|
final List<LocalAlbum> selectedBackupAlbums;
|
||||||
|
|
||||||
|
const _SelectAllButton({
|
||||||
|
required this.filteredAlbums,
|
||||||
|
required this.selectedBackupAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final canSelectAll = filteredAlbums
|
||||||
|
.where((album) => album.backupSelection != BackupSelection.selected)
|
||||||
|
.isNotEmpty;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: canSelectAll
|
||||||
|
? () {
|
||||||
|
for (final album in filteredAlbums) {
|
||||||
|
if (album.backupSelection != BackupSelection.selected) {
|
||||||
|
ref
|
||||||
|
.read(backupAlbumProvider.notifier)
|
||||||
|
.selectAlbum(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.select_all),
|
||||||
|
label: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: Text(
|
||||||
|
"select_all".t(context: context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: selectedBackupAlbums.isNotEmpty
|
||||||
|
? () {
|
||||||
|
for (final album in filteredAlbums) {
|
||||||
|
if (album.backupSelection == BackupSelection.selected) {
|
||||||
|
ref
|
||||||
|
.read(backupAlbumProvider.notifier)
|
||||||
|
.deselectAlbum(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.deselect),
|
||||||
|
label: Text('deselect_all'.t(context: context)),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftUploadDetailPage extends ConsumerWidget {
|
||||||
|
const DriftUploadDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final uploadItems = ref.watch(
|
||||||
|
driftBackupProvider.select((state) => state.uploadItems),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("upload_details".t(context: context)),
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 1,
|
||||||
|
),
|
||||||
|
body: uploadItems.isEmpty
|
||||||
|
? _buildEmptyState(context)
|
||||||
|
: _buildUploadList(uploadItems),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud_upload_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"no_uploads_in_progress".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUploadList(
|
||||||
|
Map<String, DriftUploadStatus> uploadItems,
|
||||||
|
) {
|
||||||
|
return ListView.separated(
|
||||||
|
addAutomaticKeepAlives: true,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: uploadItems.length,
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = uploadItems.values.elementAt(index);
|
||||||
|
return _buildUploadCard(context, item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUploadCard(
|
||||||
|
BuildContext context,
|
||||||
|
DriftUploadStatus item,
|
||||||
|
) {
|
||||||
|
final isCompleted = item.progress >= 1.0;
|
||||||
|
final double progressPercentage = (item.progress * 100).clamp(0, 100);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showFileDetailDialog(context, item),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
path.basename(item.filename),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Tap for more details',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildProgressIndicator(
|
||||||
|
context,
|
||||||
|
item.progress,
|
||||||
|
progressPercentage,
|
||||||
|
isCompleted,
|
||||||
|
item.networkSpeedAsString,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProgressIndicator(
|
||||||
|
BuildContext context,
|
||||||
|
double progress,
|
||||||
|
double percentage,
|
||||||
|
bool isCompleted,
|
||||||
|
String networkSpeedAsString,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
child: TweenAnimationBuilder(
|
||||||
|
tween: Tween<double>(begin: 0.0, end: progress),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => CircularProgressIndicator(
|
||||||
|
backgroundColor:
|
||||||
|
context.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
|
strokeWidth: 3,
|
||||||
|
value: value,
|
||||||
|
color: isCompleted
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCompleted)
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
percentage.toStringAsFixed(0),
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
networkSpeedAsString,
|
||||||
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showFileDetailDialog(
|
||||||
|
BuildContext context,
|
||||||
|
DriftUploadStatus item,
|
||||||
|
) async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => FileDetailDialog(uploadStatus: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDetailDialog extends ConsumerWidget {
|
||||||
|
final DriftUploadStatus uploadStatus;
|
||||||
|
|
||||||
|
const FileDetailDialog({
|
||||||
|
super.key,
|
||||||
|
required this.uploadStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.all(20),
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: context.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"details".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: FutureBuilder<LocalAsset?>(
|
||||||
|
future: _getAssetDetails(ref, uploadStatus.taskId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = snapshot.data;
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Thumbnail at the top center
|
||||||
|
Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
child: Container(
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline
|
||||||
|
.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
child: asset != null
|
||||||
|
? Thumbnail(
|
||||||
|
asset: asset,
|
||||||
|
size: const Size(512, 512),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (asset != null) ...[
|
||||||
|
_buildInfoSection(context, [
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Filename",
|
||||||
|
path.basename(uploadStatus.filename),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Local ID",
|
||||||
|
asset.id,
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"File Size",
|
||||||
|
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||||
|
),
|
||||||
|
if (asset.width != null)
|
||||||
|
_buildInfoRow(context, "Width", "${asset.width}px"),
|
||||||
|
if (asset.height != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Height",
|
||||||
|
"${asset.height}px",
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Created At",
|
||||||
|
asset.createdAt.toString(),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Updated At",
|
||||||
|
asset.updatedAt.toString(),
|
||||||
|
),
|
||||||
|
if (asset.checksum != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
"Checksum",
|
||||||
|
asset.checksum!,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(
|
||||||
|
"close".t(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoSection(
|
||||||
|
BuildContext context,
|
||||||
|
List<Widget> children,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Text(
|
||||||
|
"$label:",
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> _getAssetDetails(
|
||||||
|
WidgetRef ref,
|
||||||
|
String localAssetId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final repository = ref.read(localAssetRepository);
|
||||||
|
return await repository.getById(localAssetId);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class ChangeExperiencePage extends ConsumerStatefulWidget {
|
||||||
|
final bool switchingToBeta;
|
||||||
|
|
||||||
|
const ChangeExperiencePage({super.key, required this.switchingToBeta});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _ChangeExperiencePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
|
bool hasMigrated = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleMigration() async {
|
||||||
|
if (widget.switchingToBeta) {
|
||||||
|
final assetNotifier = ref.read(assetProvider.notifier);
|
||||||
|
if (assetNotifier.mounted) {
|
||||||
|
assetNotifier.dispose();
|
||||||
|
}
|
||||||
|
final albumNotifier = ref.read(albumProvider.notifier);
|
||||||
|
if (albumNotifier.mounted) {
|
||||||
|
albumNotifier.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||||
|
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||||
|
|
||||||
|
final permission = await ref
|
||||||
|
.read(galleryPermissionNotifier.notifier)
|
||||||
|
.requestGalleryPermission();
|
||||||
|
|
||||||
|
if (permission.isGranted) {
|
||||||
|
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
await migrateDeviceAssetToSqlite(
|
||||||
|
ref.read(isarProvider),
|
||||||
|
ref.read(driftProvider),
|
||||||
|
);
|
||||||
|
await migrateBackupAlbumsToSqlite(
|
||||||
|
ref.read(isarProvider),
|
||||||
|
ref.read(driftProvider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
|
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||||
|
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
hasMigrated = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? const Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 48.0,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
width: 50.0,
|
||||||
|
height: 50.0,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 300.0,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? Text(
|
||||||
|
"Migration success!",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
"Data migration in progress...\nPlease wait and don't close this page",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasMigrated)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.replaceRoute(
|
||||||
|
widget.switchingToBeta
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("Continue"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user