Compare commits
25 Commits
sqlite-rem
...
chore/dock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e20d7b49 | ||
|
|
97c256e89b | ||
|
|
f929dc0816 | ||
|
|
9e94f52b05 | ||
|
|
5d244c6fec | ||
|
|
dcfe8d5ade | ||
|
|
635f5de186 | ||
|
|
9719965caf | ||
|
|
f33e1ad94c | ||
|
|
576f681b5c | ||
|
|
493d85b021 | ||
|
|
f32d4f15b6 | ||
|
|
7bae49ebd5 | ||
|
|
2e63b9d951 | ||
|
|
137f0d48c0 | ||
|
|
53acf08263 | ||
|
|
f32cd74232 | ||
|
|
546f841b2c | ||
|
|
8491fe459d | ||
|
|
2046dcc5b4 | ||
|
|
03ff425664 | ||
|
|
055b930066 | ||
|
|
531515daf9 | ||
|
|
b256c51b6b | ||
|
|
238dc7c085 |
@@ -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:
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /immich-devcontainer/container-common.sh
|
source /immich-devcontainer/container-common.sh
|
||||||
|
|
||||||
log "Setting up Immich dev container..."
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
log "Installing npm dependencies (node_modules)..."
|
log "Installing npm dependencies (node_modules)..."
|
||||||
install_dependencies
|
install_dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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,22 +16,25 @@ 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
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
build:
|
build:
|
||||||
|
args:
|
||||||
|
- SERVER_USER=${SERVER_USER:-0}
|
||||||
|
- SERVER_GROUP=${SERVER_GROUP:-0}
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/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 +72,23 @@ 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
|
args:
|
||||||
command: ['/usr/src/app/bin/immich-web']
|
- WEB_USER=${WEB_USER:-1000}
|
||||||
|
- WEB_GROUP=${WEB_GROUP:-1000}
|
||||||
|
context: ../
|
||||||
|
dockerfile: web/Dockerfile
|
||||||
|
command: ['immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/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.
|
||||||
|
|||||||
@@ -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: ../
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -1691,6 +1693,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...",
|
||||||
@@ -1792,6 +1795,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",
|
||||||
@@ -1902,6 +1906,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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
9
mobile/android/app/proguard-rules.pro
vendored
9
mobile/android/app/proguard-rules.pro
vendored
@@ -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>
|
||||||
|
|||||||
@@ -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,241 @@
|
|||||||
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.glance.*
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.work.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import app.alextran.immich.widget.model.*
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class ImageDownloadWorker(
|
||||||
|
private val context: Context,
|
||||||
|
workerParameters: WorkerParameters
|
||||||
|
) : CoroutineWorker(context, workerParameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
||||||
|
|
||||||
|
private fun buildConstraints(): Constraints {
|
||||||
|
return Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putString(kWorkerWidgetType, widgetType.toString())
|
||||||
|
.putInt(kWorkerWidgetID, appWidgetId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
||||||
|
20, TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
.setConstraints(buildConstraints())
|
||||||
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
|
.addTag(appWidgetId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.enqueueUniquePeriodicWork(
|
||||||
|
"$uniqueWorkName-$appWidgetId",
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
|
||||||
|
.setConstraints(buildConstraints())
|
||||||
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
|
.addTag(appWidgetId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.enqueueUniqueWork(
|
||||||
|
"$uniqueWorkName-$appWidgetId",
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancel(context: Context, appWidgetId: Int) {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag("$uniqueWorkName-$appWidgetId")
|
||||||
|
|
||||||
|
// delete cached image
|
||||||
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
||||||
|
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentImgUUID = widgetConfig[kImageUUID]
|
||||||
|
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(currentImgUUID))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||||
|
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||||
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||||
|
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentImgUUID = widgetConfig[kImageUUID]
|
||||||
|
|
||||||
|
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||||
|
|
||||||
|
// clear any image caches and go to "login" state if no credentials
|
||||||
|
if (serverConfig == null) {
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
deleteImage(currentImgUUID)
|
||||||
|
updateWidget(
|
||||||
|
glanceId,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"immich://",
|
||||||
|
WidgetState.LOG_IN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch new image
|
||||||
|
val entry = when (widgetType) {
|
||||||
|
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
|
||||||
|
WidgetType.MEMORIES -> fetchMemory(serverConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear current image if it exists
|
||||||
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
|
deleteImage(currentImgUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save a new image
|
||||||
|
val imgUUID = UUID.randomUUID().toString()
|
||||||
|
saveImage(entry.image, imgUUID)
|
||||||
|
|
||||||
|
// trigger the update routine with new image uuid
|
||||||
|
updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(uniqueWorkName, "Error while loading image", e)
|
||||||
|
if (runAttemptCount < 10) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateWidget(
|
||||||
|
glanceId: GlanceId,
|
||||||
|
imageUUID: String,
|
||||||
|
subtitle: String?,
|
||||||
|
deeplink: String?,
|
||||||
|
widgetState: WidgetState = WidgetState.SUCCESS
|
||||||
|
) {
|
||||||
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
|
prefs[kNow] = System.currentTimeMillis()
|
||||||
|
prefs[kImageUUID] = imageUUID
|
||||||
|
prefs[kWidgetState] = widgetState.toString()
|
||||||
|
prefs[kSubtitleText] = subtitle ?: ""
|
||||||
|
prefs[kDeeplinkURL] = deeplink ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoWidget().update(context,glanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchRandom(
|
||||||
|
serverConfig: ServerConfig,
|
||||||
|
widgetConfig: Preferences
|
||||||
|
): WidgetEntry {
|
||||||
|
val api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
|
val filters = SearchFilters(AssetType.IMAGE)
|
||||||
|
val albumId = widgetConfig[kSelectedAlbum]
|
||||||
|
val showSubtitle = widgetConfig[kShowAlbumName]
|
||||||
|
val albumName = widgetConfig[kSelectedAlbumName]
|
||||||
|
var subtitle: String? = if (showSubtitle == true) albumName else ""
|
||||||
|
|
||||||
|
if (albumId != null) {
|
||||||
|
filters.albumIds = listOf(albumId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var randomSearch = api.fetchSearchResults(filters)
|
||||||
|
|
||||||
|
// handle an empty album, fallback to random
|
||||||
|
if (randomSearch.isEmpty() && albumId != null) {
|
||||||
|
randomSearch = api.fetchSearchResults(SearchFilters(AssetType.IMAGE))
|
||||||
|
subtitle = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val random = randomSearch.first()
|
||||||
|
val image = api.fetchImage(random)
|
||||||
|
|
||||||
|
return WidgetEntry(
|
||||||
|
image,
|
||||||
|
subtitle,
|
||||||
|
assetDeeplink(random)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchMemory(
|
||||||
|
serverConfig: ServerConfig
|
||||||
|
): WidgetEntry {
|
||||||
|
val api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val memories = api.fetchMemory(today)
|
||||||
|
val asset: Asset
|
||||||
|
var subtitle: String? = null
|
||||||
|
|
||||||
|
if (memories.isNotEmpty()) {
|
||||||
|
// pick a random asset from a random memory
|
||||||
|
val memory = memories.random()
|
||||||
|
asset = memory.assets.random()
|
||||||
|
|
||||||
|
val yearDiff = today.year - memory.data.year
|
||||||
|
subtitle = "$yearDiff ${if (yearDiff == 1) "year" else "years"} ago"
|
||||||
|
} else {
|
||||||
|
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
||||||
|
asset = api.fetchSearchResults(filters).first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val image = api.fetchImage(asset)
|
||||||
|
return WidgetEntry(
|
||||||
|
image,
|
||||||
|
subtitle,
|
||||||
|
assetDeeplink(asset)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
|
FileOutputStream(file).use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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")) + 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,79 @@
|
|||||||
|
package app.alextran.immich.widget.model
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.datastore.preferences.core.*
|
||||||
|
|
||||||
|
// MARK: Immich Entities
|
||||||
|
|
||||||
|
enum class AssetType {
|
||||||
|
IMAGE, VIDEO, AUDIO, OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Asset(
|
||||||
|
val id: String,
|
||||||
|
val type: AssetType,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SearchFilters(
|
||||||
|
var type: AssetType = AssetType.IMAGE,
|
||||||
|
val size: Int = 1,
|
||||||
|
var albumIds: List<String> = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MemoryResult(
|
||||||
|
val id: String,
|
||||||
|
var assets: List<Asset>,
|
||||||
|
val type: String,
|
||||||
|
val data: MemoryData
|
||||||
|
) {
|
||||||
|
data class MemoryData(val year: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Album(
|
||||||
|
val id: String,
|
||||||
|
val albumName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
// MARK: Widget Specific
|
||||||
|
|
||||||
|
enum class WidgetType {
|
||||||
|
RANDOM, MEMORIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WidgetState {
|
||||||
|
LOADING, SUCCESS, LOG_IN;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WidgetConfigState {
|
||||||
|
LOADING, SUCCESS, LOG_IN, NO_CONNECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
data class WidgetEntry (
|
||||||
|
val image: Bitmap,
|
||||||
|
val subtitle: String?,
|
||||||
|
val deeplink: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
||||||
|
|
||||||
|
// MARK: Widget State Keys
|
||||||
|
val kImageUUID = stringPreferencesKey("uuid")
|
||||||
|
val kSubtitleText = stringPreferencesKey("subtitle")
|
||||||
|
val kNow = longPreferencesKey("now")
|
||||||
|
val kWidgetState = stringPreferencesKey("state")
|
||||||
|
val kSelectedAlbum = stringPreferencesKey("albumID")
|
||||||
|
val kSelectedAlbumName = stringPreferencesKey("albumName")
|
||||||
|
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
||||||
|
val kDeeplinkURL = stringPreferencesKey("deeplink")
|
||||||
|
|
||||||
|
const val kWorkerWidgetType = "widgetType"
|
||||||
|
const val kWorkerWidgetID = "widgetId"
|
||||||
|
const val kTriggeredFromApp = "triggeredFromApp"
|
||||||
|
|
||||||
|
fun imageFilename(id: String): String {
|
||||||
|
return "widget_image_$id.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assetDeeplink(asset: Asset): String {
|
||||||
|
return "immich://asset?id=${asset.id}"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
8
mobile/android/app/src/main/res/values/strings.xml
Normal file
8
mobile/android/app/src/main/res/values/strings.xml
Normal file
@@ -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>
|
||||||
9
mobile/android/app/src/main/res/xml/memory_widget.xml
Normal file
9
mobile/android/app/src/main/res/xml/memory_widget.xml
Normal file
@@ -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"
|
||||||
|
/>
|
||||||
13
mobile/android/app/src/main/res/xml/random_widget.xml
Normal file
13
mobile/android/app/src/main/res/xml/random_widget.xml
Normal file
@@ -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'
|
||||||
|
|||||||
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
1
mobile/drift_schemas/main/drift_schema_v2.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v2.json
generated
Normal file
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,
|
||||||
|
|||||||
@@ -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>"]}}}
|
|
||||||
@@ -28,7 +28,8 @@ 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'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
38
mobile/lib/domain/models/search_result.model.dart
Normal file
38
mobile/lib/domain/models/search_result.model.dart
Normal file
@@ -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,9 @@ 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);
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
92
mobile/lib/domain/services/search.service.dart
Normal file
92
mobile/lib/domain/services/search.service.dart
Normal file
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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._();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
mobile/lib/infrastructure/entities/person.entity.dart
Normal file
34
mobile/lib/infrastructure/entities/person.entity.dart
Normal file
@@ -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};
|
||||||
|
}
|
||||||
933
mobile/lib/infrastructure/entities/person.entity.drift.dart
generated
Normal file
933
mobile/lib/infrastructure/entities/person.entity.drift.dart
generated
Normal file
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,36 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 2;
|
||||||
|
|
||||||
@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, _) async {
|
||||||
|
for (final entity in allSchemaEntities) {
|
||||||
|
await m.drop(entity);
|
||||||
|
await m.create(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
944
mobile/lib/infrastructure/repositories/db.repository.steps.dart
Normal file
944
mobile/lib/infrastructure/repositories/db.repository.steps.dart
Normal file
@@ -0,0 +1,944 @@
|
|||||||
|
// dart format width=80
|
||||||
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
|
import 'package:drift/drift.dart' as i1;
|
||||||
|
import 'dart:typed_data' as i2;
|
||||||
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
|
|
||||||
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
final class Schema2 extends i0.VersionedSchema {
|
||||||
|
Schema2({required super.database}) : super(version: 2);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
uQRemoteAssetOwnerChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
];
|
||||||
|
late final Shape0 userEntity = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape1 remoteAssetEntity = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape2 localAssetEntity = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_24,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
|
||||||
|
final i1.Index uQRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_asset_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)');
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(user_id, "key")',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(shared_by_id, shared_with_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_28,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 localAlbumEntity = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 localAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, album_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, album_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_60,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(album_id, user_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_60,
|
||||||
|
_column_25,
|
||||||
|
_column_61,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, memory_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_68,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape13 personEntity = Shape13(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_70,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape0 extends i0.VersionedTable {
|
||||||
|
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isAdmin =>
|
||||||
|
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get email =>
|
||||||
|
columnsByName['email']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get profileImagePath =>
|
||||||
|
columnsByName['profile_image_path']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get quotaSizeInBytes =>
|
||||||
|
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get quotaUsageInBytes =>
|
||||||
|
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_2(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_admin', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_admin" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('email', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('profile_image_path', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
|
||||||
|
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('quota_size_in_bytes', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape1 extends i0.VersionedTable {
|
||||||
|
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get localDateTime =>
|
||||||
|
columnsByName['local_date_time']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get thumbHash =>
|
||||||
|
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||||
|
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get stackId =>
|
||||||
|
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('type', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
|
||||||
|
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('width', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('height', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('duration_in_seconds', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('checksum', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_favorite', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_favorite" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('owner_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<DateTime> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('local_date_time', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_17(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('thumb_hash', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_20(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('visibility', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_21(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('stack_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
|
||||||
|
class Shape2 extends i0.VersionedTable {
|
||||||
|
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get orientation =>
|
||||||
|
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('checksum', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_23(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('orientation', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get primaryAssetId =>
|
||||||
|
columnsByName['primary_asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id)'));
|
||||||
|
|
||||||
|
class Shape4 extends i0.VersionedTable {
|
||||||
|
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get userId =>
|
||||||
|
columnsByName['user_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get key =>
|
||||||
|
columnsByName['key']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get value =>
|
||||||
|
columnsByName['value']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('user_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<int> _column_26(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('key', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> _column_27(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<i2.Uint8List>('value', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.blob);
|
||||||
|
|
||||||
|
class Shape5 extends i0.VersionedTable {
|
||||||
|
Shape5({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get sharedById =>
|
||||||
|
columnsByName['shared_by_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get sharedWithId =>
|
||||||
|
columnsByName['shared_with_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get inTimeline =>
|
||||||
|
columnsByName['in_timeline']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('shared_by_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_29(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('shared_with_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('in_timeline', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("in_timeline" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape6 extends i0.VersionedTable {
|
||||||
|
Shape6({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get backupSelection =>
|
||||||
|
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
|
||||||
|
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get marker_ =>
|
||||||
|
columnsByName['marker']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_31(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('backup_selection', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<bool> _column_32(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_ios_shared_album" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<bool> _column_33(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('marker', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("marker" IN (0, 1))'));
|
||||||
|
|
||||||
|
class Shape7 extends i0.VersionedTable {
|
||||||
|
Shape7({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get albumId =>
|
||||||
|
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES local_asset_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_35(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('album_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES local_album_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape8 extends i0.VersionedTable {
|
||||||
|
Shape8({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get city =>
|
||||||
|
columnsByName['city']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get state =>
|
||||||
|
columnsByName['state']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get country =>
|
||||||
|
columnsByName['country']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get dateTimeOriginal =>
|
||||||
|
columnsByName['date_time_original']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get exposureTime =>
|
||||||
|
columnsByName['exposure_time']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<double> get fNumber =>
|
||||||
|
columnsByName['f_number']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<int> get fileSize =>
|
||||||
|
columnsByName['file_size']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<double> get focalLength =>
|
||||||
|
columnsByName['focal_length']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<double> get latitude =>
|
||||||
|
columnsByName['latitude']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<double> get longitude =>
|
||||||
|
columnsByName['longitude']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<int> get iso =>
|
||||||
|
columnsByName['iso']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get make =>
|
||||||
|
columnsByName['make']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get model =>
|
||||||
|
columnsByName['model']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get lens =>
|
||||||
|
columnsByName['lens']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get orientation =>
|
||||||
|
columnsByName['orientation']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get timeZone =>
|
||||||
|
columnsByName['time_zone']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get rating =>
|
||||||
|
columnsByName['rating']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get projectionType =>
|
||||||
|
columnsByName['projection_type']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('city', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('state', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('country', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_40(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('date_time_original', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_41(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('description', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_42(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('exposure_time', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<double> _column_43(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('f_number', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<int> _column_44(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('file_size', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<double> _column_45(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('focal_length', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<double> _column_46(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('latitude', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<double> _column_47(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('longitude', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<int> _column_48(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('iso', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_49(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('make', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_50(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('model', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_51(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('lens', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_52(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('orientation', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_53(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('time_zone', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_54(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('rating', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('projection_type', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
|
||||||
|
class Shape9 extends i0.VersionedTable {
|
||||||
|
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get thumbnailAssetId =>
|
||||||
|
columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isActivityEnabled =>
|
||||||
|
columnsByName['is_activity_enabled']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get order =>
|
||||||
|
columnsByName['order']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_56(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('description', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'\''));
|
||||||
|
i1.GeneratedColumn<String> _column_57(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
|
||||||
|
i1.GeneratedColumn<bool> _column_58(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_activity_enabled" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('1'));
|
||||||
|
i1.GeneratedColumn<int> _column_59(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('order', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_60(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('album_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape10 extends i0.VersionedTable {
|
||||||
|
Shape10({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get albumId =>
|
||||||
|
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get userId =>
|
||||||
|
columnsByName['user_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get role =>
|
||||||
|
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_61(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('role', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
|
||||||
|
class Shape11 extends i0.VersionedTable {
|
||||||
|
Shape11({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get data =>
|
||||||
|
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isSaved =>
|
||||||
|
columnsByName['is_saved']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get memoryAt =>
|
||||||
|
columnsByName['memory_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get seenAt =>
|
||||||
|
columnsByName['seen_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get showAt =>
|
||||||
|
columnsByName['show_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get hideAt =>
|
||||||
|
columnsByName['hide_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_62(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('data', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_63(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_saved', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_saved" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<DateTime> _column_64(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_65(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('seen_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_66(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('show_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_67(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('hide_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
|
||||||
|
class Shape12 extends i0.VersionedTable {
|
||||||
|
Shape12({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get memoryId =>
|
||||||
|
columnsByName['memory_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_68(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('memory_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES memory_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape13 extends i0.VersionedTable {
|
||||||
|
Shape13({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get faceAssetId =>
|
||||||
|
columnsByName['face_asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get thumbnailPath =>
|
||||||
|
columnsByName['thumbnail_path']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get isHidden =>
|
||||||
|
columnsByName['is_hidden']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get color =>
|
||||||
|
columnsByName['color']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get birthDate =>
|
||||||
|
columnsByName['birth_date']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_69(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('face_asset_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_70(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('thumbnail_path', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_71(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_favorite', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_favorite" IN (0, 1))'));
|
||||||
|
i1.GeneratedColumn<bool> _column_72(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_hidden', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_hidden" IN (0, 1))'));
|
||||||
|
i1.GeneratedColumn<String> _column_73(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('color', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_74(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('birth_date', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
}) {
|
||||||
|
return (currentVersion, database) async {
|
||||||
|
switch (currentVersion) {
|
||||||
|
case 1:
|
||||||
|
final schema = Schema2(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from1To2(migrator, schema);
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.OnUpgrade stepByStep({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
}) =>
|
||||||
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
|
step: migrationSteps(
|
||||||
|
from1To2: from1To2,
|
||||||
|
));
|
||||||
@@ -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,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.id.isIn(stack.assetIds))
|
||||||
|
.map((row) => row.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
final companion = StackEntityCompanion(
|
||||||
|
ownerId: Value(userId),
|
||||||
|
primaryAssetId: Value(stack.primaryAssetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.stackEntity,
|
||||||
|
companion.copyWith(id: Value(stack.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final assetId in stack.assetIds) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(
|
||||||
|
stackId: Value(stack.id),
|
||||||
|
),
|
||||||
|
where: (e) => e.id.equals(assetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unStack(List<String> stackIds) {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
||||||
|
|
||||||
|
// TODO: delete this after adding foreign key on stackId
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
||||||
|
where: (e) => e.stackId.isIn(stackIds),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,48 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updatePeopleV1(Iterable<SyncPersonV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final person in data) {
|
||||||
|
final companion = PersonEntityCompanion(
|
||||||
|
createdAt: Value(person.createdAt),
|
||||||
|
updatedAt: Value(person.updatedAt),
|
||||||
|
ownerId: Value(person.ownerId),
|
||||||
|
name: Value(person.name),
|
||||||
|
faceAssetId: Value(person.faceAssetId),
|
||||||
|
thumbnailPath: Value(person.thumbnailPath),
|
||||||
|
isFavorite: Value(person.isFavorite),
|
||||||
|
isHidden: Value(person.isHidden),
|
||||||
|
color: Value(person.color),
|
||||||
|
birthDate: Value(person.birthDate),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.personEntity,
|
||||||
|
companion.copyWith(id: Value(person.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updatePeopleV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePeopleV1(
|
||||||
|
Iterable<SyncPersonDeleteV1> data,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _db.personEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.personId)),
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deletePeopleV1', error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AssetTypeEnum {
|
extension on AssetTypeEnum {
|
||||||
|
|||||||
@@ -89,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!,
|
||||||
@@ -165,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) => (
|
||||||
@@ -292,6 +304,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.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) =>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
132
mobile/lib/pages/common/change_experience.page.dart
Normal file
132
mobile/lib/pages/common/change_experience.page.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
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/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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
hasMigrated = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? const Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 48.0,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
width: 50.0,
|
||||||
|
height: 50.0,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 300.0,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? Text(
|
||||||
|
"Migration success!",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
"Data migration in progress...\nPlease wait and don't close this page",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasMigrated)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.replaceRoute(
|
||||||
|
widget.switchingToBeta
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("Continue"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,7 +125,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final asset = loadAsset(currentIndex.value);
|
final asset = loadAsset(currentIndex.value);
|
||||||
|
|
||||||
if (asset.isRemote) {
|
if (asset.isRemote) {
|
||||||
ref.read(castProvider.notifier).loadMedia(asset, false);
|
ref.read(castProvider.notifier).loadMediaOld(asset, false);
|
||||||
} else {
|
} else {
|
||||||
if (isCasting) {
|
if (isCasting) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -394,7 +394,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// send image to casting if the server has it
|
// send image to casting if the server has it
|
||||||
if (newAsset.isRemote) {
|
if (newAsset.isRemote) {
|
||||||
ref.read(castProvider.notifier).loadMedia(newAsset, false);
|
ref.read(castProvider.notifier).loadMediaOld(newAsset, false);
|
||||||
} else {
|
} else {
|
||||||
context.scaffoldMessenger.clearSnackBars();
|
context.scaffoldMessenger.clearSnackBars();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
|||||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||||
@@ -94,55 +95,59 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
const _MobileLayout();
|
const _MobileLayout();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
final List<Widget> settings = SettingSection.values
|
||||||
physics: const ClampingScrollPhysics(),
|
.map(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
(setting) => Padding(
|
||||||
children: SettingSection.values
|
padding: const EdgeInsets.symmetric(
|
||||||
.map(
|
horizontal: 16.0,
|
||||||
(setting) => Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
child: Card(
|
||||||
horizontal: 16.0,
|
elevation: 0,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
child: Card(
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
elevation: 0,
|
child: ListTile(
|
||||||
clipBehavior: Clip.antiAlias,
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
color: context.colorScheme.surfaceContainer,
|
horizontal: 16.0,
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
leading: Container(
|
||||||
child: ListTile(
|
decoration: BoxDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
horizontal: 16.0,
|
color: context.isDarkTheme
|
||||||
|
? Colors.black26
|
||||||
|
: Colors.white.withAlpha(100),
|
||||||
),
|
),
|
||||||
leading: Container(
|
padding: const EdgeInsets.all(16.0),
|
||||||
decoration: BoxDecoration(
|
child: Icon(setting.icon, color: context.primaryColor),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
color: context.isDarkTheme
|
|
||||||
? Colors.black26
|
|
||||||
: Colors.white.withAlpha(100),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Icon(setting.icon, color: context.primaryColor),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
setting.title,
|
|
||||||
style: context.textTheme.titleMedium!.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
subtitle: Text(
|
|
||||||
setting.subtitle,
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
).tr(),
|
|
||||||
onTap: () =>
|
|
||||||
context.pushRoute(SettingsSubRoute(section: setting)),
|
|
||||||
),
|
),
|
||||||
|
title: Text(
|
||||||
|
setting.title,
|
||||||
|
style: context.textTheme.titleMedium!.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
setting.subtitle,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
).tr(),
|
||||||
|
onTap: () =>
|
||||||
|
context.pushRoute(SettingsSubRoute(section: setting)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.toList(),
|
)
|
||||||
|
.toList();
|
||||||
|
return ListView(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
|
||||||
|
children: [
|
||||||
|
const BetaTimelineListTile(),
|
||||||
|
...settings,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,15 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.router.current.name == SplashScreenRoute.name) {
|
if (context.router.current.name == SplashScreenRoute.name) {
|
||||||
context.replaceRoute(const TabControllerRoute());
|
context.replaceRoute(
|
||||||
|
Store.isBetaTimelineEnabled
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasPermission =
|
final hasPermission =
|
||||||
|
|||||||
@@ -9,39 +9,31 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabShellPage extends ConsumerWidget {
|
class TabShellPage extends ConsumerStatefulWidget {
|
||||||
const TabShellPage({super.key});
|
const TabShellPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<TabShellPage> createState() => _TabShellPageState();
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
}
|
||||||
|
|
||||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||||
if (!isProcessing) return icon;
|
@override
|
||||||
return Stack(
|
void initState() {
|
||||||
alignment: Alignment.center,
|
super.initState();
|
||||||
clipBehavior: Clip.none,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
children: [
|
ref.read(websocketProvider.notifier).connect();
|
||||||
icon,
|
runNewSync(ref, full: true);
|
||||||
Positioned(
|
});
|
||||||
right: -18,
|
}
|
||||||
child: SizedBox(
|
|
||||||
height: 20,
|
@override
|
||||||
width: 20,
|
Widget build(BuildContext context) {
|
||||||
child: CircularProgressIndicator(
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final navigationDestinations = [
|
final navigationDestinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
@@ -49,12 +41,9 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_library_outlined,
|
Icons.photo_library_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: Icon(
|
||||||
isProcessing: false,
|
Icons.photo_library,
|
||||||
icon: Icon(
|
color: context.primaryColor,
|
||||||
Icons.photo_library,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
@@ -72,12 +61,9 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_album_outlined,
|
Icons.photo_album_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: Icon(
|
||||||
isProcessing: false,
|
Icons.photo_album_rounded,
|
||||||
icon: Icon(
|
color: context.primaryColor,
|
||||||
Icons.photo_album_rounded,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
@@ -85,12 +71,9 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.space_dashboard_outlined,
|
Icons.space_dashboard_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: Icon(
|
||||||
isProcessing: false,
|
Icons.space_dashboard_rounded,
|
||||||
icon: Icon(
|
color: context.primaryColor,
|
||||||
Icons.space_dashboard_rounded,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -117,7 +100,7 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const MainTimelineRoute(),
|
const MainTimelineRoute(),
|
||||||
SearchRoute(),
|
DriftSearchRoute(),
|
||||||
const DriftAlbumsRoute(),
|
const DriftAlbumsRoute(),
|
||||||
const DriftLibraryRoute(),
|
const DriftLibraryRoute(),
|
||||||
],
|
],
|
||||||
@@ -167,7 +150,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
|||||||
|
|
||||||
// Album page
|
// Album page
|
||||||
if (index == 2) {
|
if (index == 2) {
|
||||||
ref.read(remoteAlbumProvider.notifier).getAll();
|
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart' show useState;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/local_auth.provider.dart';
|
import 'package:immich_mobile/providers/local_auth.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/forms/pin_registration_form.dart';
|
import 'package:immich_mobile/widgets/forms/pin_registration_form.dart';
|
||||||
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
|
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PinAuthPage extends HookConsumerWidget {
|
class PinAuthPage extends HookConsumerWidget {
|
||||||
@@ -19,6 +20,7 @@ class PinAuthPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final localAuthState = ref.watch(localAuthProvider);
|
final localAuthState = ref.watch(localAuthProvider);
|
||||||
final showPinRegistrationForm = useState(createPinCode);
|
final showPinRegistrationForm = useState(createPinCode);
|
||||||
|
final isBetaTimeline = Store.isBetaTimelineEnabled;
|
||||||
|
|
||||||
Future<void> registerBiometric(String pinCode) async {
|
Future<void> registerBiometric(String pinCode) async {
|
||||||
final isRegistered =
|
final isRegistered =
|
||||||
@@ -39,7 +41,11 @@ class PinAuthPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
context.replaceRoute(const LockedRoute());
|
if (isBetaTimeline) {
|
||||||
|
context.replaceRoute(const DriftLockedFolderRoute());
|
||||||
|
} else {
|
||||||
|
context.replaceRoute(const LockedRoute());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +99,14 @@ class PinAuthPage extends HookConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: PinVerificationForm(
|
child: PinVerificationForm(
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
onSuccess: (_) =>
|
onSuccess: (_) {
|
||||||
context.replaceRoute(const LockedRoute()),
|
if (isBetaTimeline) {
|
||||||
|
context
|
||||||
|
.replaceRoute(const DriftLockedFolderRoute());
|
||||||
|
} else {
|
||||||
|
context.replaceRoute(const LockedRoute());
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
showPeoplePicker() {
|
showPeoplePicker() {
|
||||||
handleOnSelect(Set<Person> value) {
|
handleOnSelect(Set<PersonDto> value) {
|
||||||
filter.value = filter.value.copyWith(
|
filter.value = filter.value.copyWith(
|
||||||
people: value,
|
people: value,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
@@ -75,7 +76,9 @@ class ShareIntentPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
const TabControllerRoute(),
|
Store.isBetaTimelineEnabled
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
|||||||
@@ -22,16 +22,6 @@ final _features = [
|
|||||||
icon: Icons.timeline_rounded,
|
icon: Icons.timeline_rounded,
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||||
),
|
),
|
||||||
_Feature(
|
|
||||||
name: 'Video',
|
|
||||||
icon: Icons.video_collection_outlined,
|
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
|
||||||
),
|
|
||||||
_Feature(
|
|
||||||
name: 'Recently Taken',
|
|
||||||
icon: Icons.schedule_outlined,
|
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
|
|
||||||
),
|
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Selection Mode Timeline',
|
name: 'Selection Mode Timeline',
|
||||||
icon: Icons.developer_mode_rounded,
|
icon: Icons.developer_mode_rounded,
|
||||||
@@ -122,6 +112,7 @@ final _features = [
|
|||||||
await db.memoryEntity.deleteAll();
|
await db.memoryEntity.deleteAll();
|
||||||
await db.memoryAssetEntity.deleteAll();
|
await db.memoryAssetEntity.deleteAll();
|
||||||
await db.stackEntity.deleteAll();
|
await db.stackEntity.deleteAll();
|
||||||
|
await db.personEntity.deleteAll();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
|
|
||||||
return memoryLaneProvider.when(
|
return memoryLaneProvider.maybeWhen(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty
|
return memories.isEmpty
|
||||||
? const Timeline(showStorageIndicator: true)
|
? const Timeline(showStorageIndicator: true)
|
||||||
@@ -26,8 +26,7 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
showStorageIndicator: true,
|
showStorageIndicator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Timeline(showStorageIndicator: true),
|
orElse: () => const Timeline(showStorageIndicator: true),
|
||||||
error: (error, stackTrace) => const Timeline(showStorageIndicator: true),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ final _remoteStats = [
|
|||||||
name: 'Stacks',
|
name: 'Stacks',
|
||||||
load: (db) => db.managers.stackEntity.count(),
|
load: (db) => db.managers.stackEntity.count(),
|
||||||
),
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'People',
|
||||||
|
load: (db) => db.managers.personEntity.count(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
|
|
||||||
// Load albums when component mounts
|
// Load albums when component mounts
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
ref.read(remoteAlbumProvider.notifier).getAll();
|
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
@@ -88,17 +88,20 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final albumState = ref.watch(remoteAlbumProvider);
|
final albums =
|
||||||
final albums = albumState.filteredAlbums;
|
ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||||
final isLoading = albumState.isLoading;
|
|
||||||
final error = albumState.error;
|
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
|
edgeOffset: 100,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
ImmichSliverAppBar(
|
ImmichSliverAppBar(
|
||||||
|
snap: false,
|
||||||
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -133,14 +136,10 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
? _AlbumGrid(
|
? _AlbumGrid(
|
||||||
albums: albums,
|
albums: albums,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isLoading: isLoading,
|
|
||||||
error: error,
|
|
||||||
)
|
)
|
||||||
: _AlbumList(
|
: _AlbumList(
|
||||||
albums: albums,
|
albums: albums,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isLoading: isLoading,
|
|
||||||
error: error,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -481,46 +480,15 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
|||||||
|
|
||||||
class _AlbumList extends ConsumerWidget {
|
class _AlbumList extends ConsumerWidget {
|
||||||
const _AlbumList({
|
const _AlbumList({
|
||||||
required this.isLoading,
|
|
||||||
required this.error,
|
|
||||||
required this.albums,
|
required this.albums,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isLoading;
|
|
||||||
final String? error;
|
|
||||||
final List<RemoteAlbum> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
if (isLoading) {
|
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(20.0),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: Text(
|
|
||||||
'Error loading albums: $error',
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albums.isEmpty) {
|
if (albums.isEmpty) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -623,44 +591,13 @@ class _AlbumGrid extends StatelessWidget {
|
|||||||
const _AlbumGrid({
|
const _AlbumGrid({
|
||||||
required this.albums,
|
required this.albums,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.isLoading,
|
|
||||||
required this.error,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<RemoteAlbum> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
final bool isLoading;
|
|
||||||
final String? error;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isLoading) {
|
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(20.0),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: Text(
|
|
||||||
'Error loading albums: $error',
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albums.isEmpty) {
|
if (albums.isEmpty) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@@ -4,14 +4,45 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftLockedFolderPage extends StatelessWidget {
|
class DriftLockedFolderPage extends ConsumerStatefulWidget {
|
||||||
const DriftLockedFolderPage({super.key});
|
const DriftLockedFolderPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftLockedFolderPage> createState() =>
|
||||||
|
_DriftLockedFolderPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftLockedFolderPageState extends ConsumerState<DriftLockedFolderPage>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
bool _showOverlay = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showOverlay = state != AppLifecycleState.resumed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
@@ -30,12 +61,18 @@ class DriftLockedFolderPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: _showOverlay
|
||||||
appBar: MesmerizingSliverAppBar(
|
? const SizedBox()
|
||||||
title: 'locked_folder'.t(context: context),
|
: PopScope(
|
||||||
),
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
bottomSheet: const LockedFolderBottomSheet(),
|
didPop ? ref.read(authProvider.notifier).lockPinCode() : null,
|
||||||
),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: 'locked_folder'.t(context: context),
|
||||||
|
),
|
||||||
|
bottomSheet: const LockedFolderBottomSheet(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftRecentlyTakenPage extends StatelessWidget {
|
class DriftRecentlyTakenPage extends StatelessWidget {
|
||||||
@@ -29,7 +31,9 @@ class DriftRecentlyTakenPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftVideoPage extends StatelessWidget {
|
class DriftVideoPage extends StatelessWidget {
|
||||||
@@ -27,7 +29,9 @@ class DriftVideoPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(title: 'videos'.t()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class LocalTimelinePage extends StatelessWidget {
|
|||||||
child: Timeline(
|
child: Timeline(
|
||||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||||
bottomSheet: const LocalAlbumBottomSheet(),
|
bottomSheet: const LocalAlbumBottomSheet(),
|
||||||
|
showStorageIndicator: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
925
mobile/lib/presentation/pages/search/drift_search.page.dart
Normal file
925
mobile/lib/presentation/pages/search/drift_search.page.dart
Normal file
@@ -0,0 +1,925 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/location_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftSearchPage extends HookConsumerWidget {
|
||||||
|
const DriftSearchPage({super.key, this.preFilter});
|
||||||
|
|
||||||
|
final SearchFilter? preFilter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final textSearchType = useState<TextSearchType>(TextSearchType.context);
|
||||||
|
final searchHintText =
|
||||||
|
useState<String>('sunrise_on_the_beach'.t(context: context));
|
||||||
|
final textSearchController = useTextEditingController();
|
||||||
|
final filter = useState<SearchFilter>(
|
||||||
|
SearchFilter(
|
||||||
|
people: preFilter?.people ?? {},
|
||||||
|
location: preFilter?.location ?? SearchLocationFilter(),
|
||||||
|
camera: preFilter?.camera ?? SearchCameraFilter(),
|
||||||
|
date: preFilter?.date ?? SearchDateFilter(),
|
||||||
|
display: preFilter?.display ??
|
||||||
|
SearchDisplayFilters(
|
||||||
|
isNotInAlbum: false,
|
||||||
|
isArchive: false,
|
||||||
|
isFavorite: false,
|
||||||
|
),
|
||||||
|
mediaType: preFilter?.mediaType ?? AssetType.other,
|
||||||
|
language:
|
||||||
|
"${context.locale.languageCode}-${context.locale.countryCode}",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final previousFilter = useState<SearchFilter?>(null);
|
||||||
|
|
||||||
|
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
final cameraCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
final locationCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
final displayOptionCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
|
||||||
|
final isSearching = useState(false);
|
||||||
|
|
||||||
|
SnackBar searchInfoSnackBar(String message) {
|
||||||
|
return SnackBar(
|
||||||
|
content: Text(
|
||||||
|
message,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
showCloseIcon: true,
|
||||||
|
behavior: SnackBarBehavior.fixed,
|
||||||
|
closeIconColor: context.colorScheme.onSurface,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
search() async {
|
||||||
|
if (filter.value.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preFilter == null && filter.value == previousFilter.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching.value = true;
|
||||||
|
ref.watch(paginatedSearchProvider.notifier).clear();
|
||||||
|
final hasResult = await ref
|
||||||
|
.watch(paginatedSearchProvider.notifier)
|
||||||
|
.search(filter.value);
|
||||||
|
|
||||||
|
if (!hasResult) {
|
||||||
|
context.showSnackBar(
|
||||||
|
searchInfoSnackBar('search_no_result'.t(context: context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFilter.value = filter.value;
|
||||||
|
isSearching.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreSearchResult() async {
|
||||||
|
isSearching.value = true;
|
||||||
|
final hasResult = await ref
|
||||||
|
.watch(paginatedSearchProvider.notifier)
|
||||||
|
.search(filter.value);
|
||||||
|
|
||||||
|
if (!hasResult) {
|
||||||
|
context.showSnackBar(
|
||||||
|
searchInfoSnackBar('search_no_more_result'.t(context: context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPreFilter() {
|
||||||
|
if (preFilter != null) {
|
||||||
|
Future.delayed(
|
||||||
|
Duration.zero,
|
||||||
|
() {
|
||||||
|
search();
|
||||||
|
|
||||||
|
if (preFilter!.location.city != null) {
|
||||||
|
locationCurrentFilterWidget.value = Text(
|
||||||
|
preFilter!.location.city!,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
Future.microtask(
|
||||||
|
() => ref.invalidate(paginatedSearchProvider),
|
||||||
|
);
|
||||||
|
searchPreFilter();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
showPeoplePicker() {
|
||||||
|
handleOnSelect(Set<PersonDto> value) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
people: value,
|
||||||
|
);
|
||||||
|
|
||||||
|
peopleCurrentFilterWidget.value = Text(
|
||||||
|
value
|
||||||
|
.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context))
|
||||||
|
.join(', '),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
people: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
peopleCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
heightFactor: 0.8,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'search_filter_people_title'.t(context: context),
|
||||||
|
expanded: true,
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: PeoplePicker(
|
||||||
|
onSelect: handleOnSelect,
|
||||||
|
filter: filter.value.people,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLocationPicker() {
|
||||||
|
handleOnSelect(Map<String, String?> value) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
location: SearchLocationFilter(
|
||||||
|
country: value['country'],
|
||||||
|
city: value['city'],
|
||||||
|
state: value['state'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final locationText = <String>[];
|
||||||
|
if (value['country'] != null) {
|
||||||
|
locationText.add(value['country']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value['state'] != null) {
|
||||||
|
locationText.add(value['state']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value['city'] != null) {
|
||||||
|
locationText.add(value['city']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
locationCurrentFilterWidget.value = Text(
|
||||||
|
locationText.join(', '),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
location: SearchLocationFilter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
locationCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
isDismissible: true,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'search_filter_location_title'.t(context: context),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: context.viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: LocationPicker(
|
||||||
|
onSelected: handleOnSelect,
|
||||||
|
filter: filter.value.location,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showCameraPicker() {
|
||||||
|
handleOnSelect(Map<String, String?> value) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
camera: SearchCameraFilter(
|
||||||
|
make: value['make'],
|
||||||
|
model: value['model'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraCurrentFilterWidget.value = Text(
|
||||||
|
'${value['make'] ?? ''} ${value['model'] ?? ''}',
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
camera: SearchCameraFilter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
isDismissible: true,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'search_filter_camera_title'.t(context: context),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: CameraPicker(
|
||||||
|
onSelect: handleOnSelect,
|
||||||
|
filter: filter.value.camera,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showDatePicker() async {
|
||||||
|
final firstDate = DateTime(1900);
|
||||||
|
final lastDate = DateTime.now();
|
||||||
|
|
||||||
|
final date = await showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
currentDate: DateTime.now(),
|
||||||
|
initialDateRange: DateTimeRange(
|
||||||
|
start: filter.value.date.takenAfter ?? lastDate,
|
||||||
|
end: filter.value.date.takenBefore ?? lastDate,
|
||||||
|
),
|
||||||
|
helpText: 'search_filter_date_title'.t(context: context),
|
||||||
|
cancelText: 'cancel'.t(context: context),
|
||||||
|
confirmText: 'select'.t(context: context),
|
||||||
|
saveText: 'save'.t(context: context),
|
||||||
|
errorFormatText: 'invalid_date_format'.t(context: context),
|
||||||
|
errorInvalidText: 'invalid_date'.t(context: context),
|
||||||
|
fieldStartHintText: 'start_date'.t(context: context),
|
||||||
|
fieldEndHintText: 'end_date'.t(context: context),
|
||||||
|
initialEntryMode: DatePickerEntryMode.calendar,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (date == null) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
date: SearchDateFilter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
dateRangeCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
date: SearchDateFilter(
|
||||||
|
takenAfter: date.start,
|
||||||
|
takenBefore: date.end.add(
|
||||||
|
const Duration(
|
||||||
|
hours: 23,
|
||||||
|
minutes: 59,
|
||||||
|
seconds: 59,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If date range is less than 24 hours, set the end date to the end of the day
|
||||||
|
if (date.end.difference(date.start).inHours < 24) {
|
||||||
|
dateRangeCurrentFilterWidget.value = Text(
|
||||||
|
DateFormat.yMMMd().format(date.start.toLocal()),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dateRangeCurrentFilterWidget.value = Text(
|
||||||
|
'search_filter_date_interval'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
||||||
|
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MEDIA PICKER
|
||||||
|
showMediaTypePicker() {
|
||||||
|
handleOnSelected(AssetType assetType) {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
mediaType: assetType,
|
||||||
|
);
|
||||||
|
|
||||||
|
mediaTypeCurrentFilterWidget.value = Text(
|
||||||
|
assetType == AssetType.image
|
||||||
|
? 'image'.t(context: context)
|
||||||
|
: assetType == AssetType.video
|
||||||
|
? 'video'.t(context: context)
|
||||||
|
: 'all'.t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
mediaType: AssetType.other,
|
||||||
|
);
|
||||||
|
|
||||||
|
mediaTypeCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'search_filter_media_type_title'.t(context: context),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: MediaTypePicker(
|
||||||
|
onSelect: handleOnSelected,
|
||||||
|
filter: filter.value.mediaType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DISPLAY OPTION
|
||||||
|
showDisplayOptionPicker() {
|
||||||
|
handleOnSelect(Map<DisplayOption, bool> value) {
|
||||||
|
final filterText = <String>[];
|
||||||
|
value.forEach((key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case DisplayOption.notInAlbum:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: filter.value.display.copyWith(
|
||||||
|
isNotInAlbum: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
filterText.add(
|
||||||
|
'search_filter_display_option_not_in_album'
|
||||||
|
.t(context: context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DisplayOption.archive:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: filter.value.display.copyWith(
|
||||||
|
isArchive: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
filterText.add('archive'.t(context: context));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DisplayOption.favorite:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: filter.value.display.copyWith(
|
||||||
|
isFavorite: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
filterText.add('favorite'.t(context: context));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filterText.isEmpty) {
|
||||||
|
displayOptionCurrentFilterWidget.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayOptionCurrentFilterWidget.value = Text(
|
||||||
|
filterText.join(', '),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
display: SearchDisplayFilters(
|
||||||
|
isNotInAlbum: false,
|
||||||
|
isArchive: false,
|
||||||
|
isFavorite: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
displayOptionCurrentFilterWidget.value = null;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: 'display_options'.t(context: context),
|
||||||
|
onSearch: search,
|
||||||
|
onClear: handleClear,
|
||||||
|
child: DisplayOptionPicker(
|
||||||
|
onSelect: handleOnSelect,
|
||||||
|
filter: filter.value.display,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTextSubmitted(String value) {
|
||||||
|
switch (textSearchType.value) {
|
||||||
|
case TextSearchType.context:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
filename: '',
|
||||||
|
context: value,
|
||||||
|
description: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TextSearchType.filename:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
filename: value,
|
||||||
|
context: '',
|
||||||
|
description: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TextSearchType.description:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
filename: '',
|
||||||
|
context: '',
|
||||||
|
description: value,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData getSearchPrefixIcon() => switch (textSearchType.value) {
|
||||||
|
TextSearchType.context => Icons.image_search_rounded,
|
||||||
|
TextSearchType.filename => Icons.abc_rounded,
|
||||||
|
TextSearchType.description => Icons.text_snippet_outlined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: MenuAnchor(
|
||||||
|
style: MenuStyle(
|
||||||
|
elevation: const WidgetStatePropertyAll(1),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
MenuController controller,
|
||||||
|
Widget? child,
|
||||||
|
) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
tooltip: 'Show text search menu',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.image_search_rounded),
|
||||||
|
title: Text(
|
||||||
|
'search_by_context'.t(context: context),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textSearchType.value == TextSearchType.context
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected: textSearchType.value == TextSearchType.context,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.context;
|
||||||
|
searchHintText.value =
|
||||||
|
'sunrise_on_the_beach'.t(context: context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.abc_rounded),
|
||||||
|
title: Text(
|
||||||
|
'search_filter_filename'.t(context: context),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textSearchType.value == TextSearchType.filename
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected: textSearchType.value == TextSearchType.filename,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.filename;
|
||||||
|
searchHintText.value =
|
||||||
|
'file_name_or_extension'.t(context: context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.text_snippet_outlined),
|
||||||
|
title: Text(
|
||||||
|
'search_by_description'.t(context: context),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color:
|
||||||
|
textSearchType.value == TextSearchType.description
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected:
|
||||||
|
textSearchType.value == TextSearchType.description,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.description;
|
||||||
|
searchHintText.value =
|
||||||
|
'search_by_description_example'.t(context: context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
title: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.09),
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SearchField(
|
||||||
|
hintText: searchHintText.value,
|
||||||
|
key: const Key('search_text_field'),
|
||||||
|
controller: textSearchController,
|
||||||
|
contentPadding: preFilter != null
|
||||||
|
? const EdgeInsets.only(left: 24)
|
||||||
|
: const EdgeInsets.all(8),
|
||||||
|
prefixIcon: preFilter != null
|
||||||
|
? null
|
||||||
|
: Icon(
|
||||||
|
getSearchPrefixIcon(),
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
onSubmitted: handleTextSubmitted,
|
||||||
|
focusNode: ref.watch(searchInputFocusProvider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ListView(
|
||||||
|
key: const Key('search_filter_chip_list'),
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
children: [
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.people_alt_outlined,
|
||||||
|
onTap: showPeoplePicker,
|
||||||
|
label: 'people'.t(context: context),
|
||||||
|
currentFilter: peopleCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.location_on_outlined,
|
||||||
|
onTap: showLocationPicker,
|
||||||
|
label: 'search_filter_location'.t(context: context),
|
||||||
|
currentFilter: locationCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.camera_alt_outlined,
|
||||||
|
onTap: showCameraPicker,
|
||||||
|
label: 'camera'.t(context: context),
|
||||||
|
currentFilter: cameraCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.date_range_outlined,
|
||||||
|
onTap: showDatePicker,
|
||||||
|
label: 'search_filter_date'.t(context: context),
|
||||||
|
currentFilter: dateRangeCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
key: const Key('media_type_chip'),
|
||||||
|
icon: Icons.video_collection_outlined,
|
||||||
|
onTap: showMediaTypePicker,
|
||||||
|
label: 'search_filter_media_type'.t(context: context),
|
||||||
|
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
SearchFilterChip(
|
||||||
|
icon: Icons.display_settings_outlined,
|
||||||
|
onTap: showDisplayOptionPicker,
|
||||||
|
label:
|
||||||
|
'search_filter_display_options'.t(context: context),
|
||||||
|
currentFilter: displayOptionCurrentFilterWidget.value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSearching.value)
|
||||||
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_SearchResultGrid(onScrollEnd: loadMoreSearchResult),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchResultGrid extends ConsumerWidget {
|
||||||
|
final VoidCallback onScrollEnd;
|
||||||
|
|
||||||
|
const _SearchResultGrid({required this.onScrollEnd});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final searchResult = ref.watch(paginatedSearchProvider);
|
||||||
|
|
||||||
|
if (searchResult.totalAssets == 0) {
|
||||||
|
return const _SearchEmptyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationListener<ScrollEndNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
final isBottomSheetNotification = notification.context
|
||||||
|
?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() !=
|
||||||
|
null;
|
||||||
|
|
||||||
|
final metrics = notification.metrics;
|
||||||
|
final isVerticalScroll = metrics.axis == Axis.vertical;
|
||||||
|
|
||||||
|
if (metrics.pixels >= metrics.maxScrollExtent &&
|
||||||
|
isVerticalScroll &&
|
||||||
|
!isBottomSheetNotification) {
|
||||||
|
onScrollEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: SliverFillRemaining(
|
||||||
|
child: ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService = ref
|
||||||
|
.watch(timelineFactoryProvider)
|
||||||
|
.fromAssets(searchResult.assets);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Timeline(
|
||||||
|
key: ValueKey(searchResult.totalAssets),
|
||||||
|
appBar: null,
|
||||||
|
groupBy: GroupAssetsBy.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchEmptyContent extends StatelessWidget {
|
||||||
|
const _SearchEmptyContent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Center(
|
||||||
|
child: Image.asset(
|
||||||
|
context.isDarkTheme
|
||||||
|
? 'assets/polaroid-dark.png'
|
||||||
|
: 'assets/polaroid-light.png',
|
||||||
|
height: 125,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'search_page_search_photos_videos'.t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: _QuickLinkList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickLinkList extends StatelessWidget {
|
||||||
|
const _QuickLinkList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
context.colorScheme.primary.withAlpha(20),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
_QuickLink(
|
||||||
|
title: 'recently_taken'.t(context: context),
|
||||||
|
icon: Icons.schedule_outlined,
|
||||||
|
isTop: true,
|
||||||
|
onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()),
|
||||||
|
),
|
||||||
|
_QuickLink(
|
||||||
|
title: 'videos'.t(context: context),
|
||||||
|
icon: Icons.play_circle_outline_rounded,
|
||||||
|
onTap: () => context.pushRoute(const DriftVideoRoute()),
|
||||||
|
),
|
||||||
|
_QuickLink(
|
||||||
|
title: 'favorites'.t(context: context),
|
||||||
|
icon: Icons.favorite_border_rounded,
|
||||||
|
isBottom: true,
|
||||||
|
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickLink extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool isTop;
|
||||||
|
final bool isBottom;
|
||||||
|
|
||||||
|
const _QuickLink({
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
this.isTop = false,
|
||||||
|
this.isBottom = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final borderRadius = BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(isTop ? 20 : 0),
|
||||||
|
topRight: Radius.circular(isTop ? 20 : 0),
|
||||||
|
bottomLeft: Radius.circular(isBottom ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(isBottom ? 20 : 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
icon,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
||||||
|
|
||||||
|
final paginatedSearchProvider =
|
||||||
|
StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
|
||||||
|
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||||
|
final SearchService _searchService;
|
||||||
|
|
||||||
|
PaginatedSearchNotifier(this._searchService)
|
||||||
|
: super(const SearchResult(assets: [], nextPage: 1));
|
||||||
|
|
||||||
|
Future<bool> search(SearchFilter filter) async {
|
||||||
|
if (state.nextPage == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _searchService.search(filter, state.nextPage!);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = SearchResult(
|
||||||
|
assets: [...state.assets, ...result.assets],
|
||||||
|
nextPage: result.nextPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
state = const SearchResult(assets: [], nextPage: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.iconData,
|
required this.iconData,
|
||||||
|
this.iconColor,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.onLongPressed,
|
this.onLongPressed,
|
||||||
this.maxWidth = 90.0,
|
this.maxWidth = 90.0,
|
||||||
@@ -15,6 +16,7 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
final Color? iconColor;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
final double? minWidth;
|
final double? minWidth;
|
||||||
final bool menuItem;
|
final bool menuItem;
|
||||||
@@ -27,7 +29,8 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||||
final iconTheme = IconTheme.of(context);
|
final iconTheme = IconTheme.of(context);
|
||||||
final iconSize = iconTheme.size ?? 24.0;
|
final iconSize = iconTheme.size ?? 24.0;
|
||||||
final iconColor = iconTheme.color ?? context.themeData.iconTheme.color;
|
final iconColor =
|
||||||
|
this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
||||||
final textColor = context.themeData.textTheme.labelLarge?.color;
|
final textColor = context.themeData.textTheme.labelLarge?.color;
|
||||||
|
|
||||||
if (menuItem) {
|
if (menuItem) {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
|
|
||||||
|
class CastActionButton extends ConsumerWidget {
|
||||||
|
const CastActionButton({super.key, this.menuItem = true});
|
||||||
|
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
|
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
|
||||||
|
iconColor:
|
||||||
|
isCasting ? context.primaryColor : null, // null = default color
|
||||||
|
label: "cast".t(context: context),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const CastDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
menuItem: menuItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,49 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class ShareActionButton extends ConsumerWidget {
|
class ShareActionButton extends ConsumerWidget {
|
||||||
const ShareActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const ShareActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).shareAssets(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
} else if (result.count > 0) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'share_action_prompt'
|
||||||
|
.t(context: context, args: {'count': result.count.toString()}),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -14,6 +51,7 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
iconData:
|
iconData:
|
||||||
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||||
label: 'share'.t(context: context),
|
label: 'share'.t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class StackActionButton extends ConsumerWidget {
|
class StackActionButton extends ConsumerWidget {
|
||||||
const StackActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const StackActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access stack action');
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await ref.read(actionProvider.notifier).stack(user.id, source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'stack_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.filter_none_rounded,
|
iconData: Icons.filter_none_rounded,
|
||||||
label: "stack".t(context: context),
|
label: "stack".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class UnStackActionButton extends ConsumerWidget {
|
||||||
|
final ActionSource source;
|
||||||
|
|
||||||
|
const UnStackActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).unStack(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'unstack_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.filter_none_rounded,
|
||||||
|
label: "unstack".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
|
class StackChildrenNotifier
|
||||||
|
extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset?> {
|
||||||
|
@override
|
||||||
|
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
|
||||||
|
if (asset == null ||
|
||||||
|
asset is! RemoteAsset ||
|
||||||
|
asset.stackId == null ||
|
||||||
|
// The stackCount check is to ensure we only fetch stacks for timelines that have stacks
|
||||||
|
asset.stackCount == 0) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(assetServiceProvider).getStack(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
|
||||||
|
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(
|
||||||
|
StackChildrenNotifier.new,
|
||||||
|
);
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
|
||||||
|
class AssetStackRow extends ConsumerWidget {
|
||||||
|
const AssetStackRow({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
int opacity = ref.watch(
|
||||||
|
assetViewerProvider.select((state) => state.backgroundOpacity),
|
||||||
|
);
|
||||||
|
final showControls =
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
|
|
||||||
|
if (!showControls) {
|
||||||
|
opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset));
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: opacity < 255,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: opacity / 255,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: ref.watch(stackChildrenNotifier(asset)).when(
|
||||||
|
data: (state) => SizedBox.square(
|
||||||
|
dimension: 80,
|
||||||
|
child: _StackList(stack: state),
|
||||||
|
),
|
||||||
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StackList extends ConsumerWidget {
|
||||||
|
final List<RemoteAsset> stack;
|
||||||
|
|
||||||
|
const _StackList({required this.stack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5,
|
||||||
|
right: 5,
|
||||||
|
bottom: 30,
|
||||||
|
),
|
||||||
|
itemCount: stack.length,
|
||||||
|
itemBuilder: (ctx, index) {
|
||||||
|
final asset = stack[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ref.read(assetViewerProvider.notifier).setStackIndex(index);
|
||||||
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
decoration: index ==
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.stackIndex))
|
||||||
|
? const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||||
|
border: Border.fromBorderSide(
|
||||||
|
BorderSide(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||||
|
border: null,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Image(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: getThumbnailImageProvider(
|
||||||
|
remoteId: asset.id,
|
||||||
|
size: const Size.square(60),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (asset.isVideo)
|
||||||
|
const Icon(
|
||||||
|
Icons.play_circle_outline_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Color.fromRGBO(0, 0, 0, 0.6),
|
||||||
|
offset: Offset(0.0, 0.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
|
||||||
@@ -18,8 +22,10 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
|
|||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
@@ -83,6 +89,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
double previousExtent = _kBottomSheetMinimumExtent;
|
double previousExtent = _kBottomSheetMinimumExtent;
|
||||||
Offset dragDownPosition = Offset.zero;
|
Offset dragDownPosition = Offset.zero;
|
||||||
int totalAssets = 0;
|
int totalAssets = 0;
|
||||||
|
int stackIndex = 0;
|
||||||
BuildContext? scaffoldContext;
|
BuildContext? scaffoldContext;
|
||||||
Map<String, GlobalKey> videoPlayerKeys = {};
|
Map<String, GlobalKey> videoPlayerKeys = {};
|
||||||
|
|
||||||
@@ -165,6 +172,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
void _onAssetChanged(int index) {
|
void _onAssetChanged(int index) {
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
// Always holds the current asset from the timeline
|
||||||
|
ref.read(assetViewerProvider.notifier).setAsset(asset);
|
||||||
|
// The currentAssetNotifier actually holds the current asset that is displayed
|
||||||
|
// which could be stack children as well
|
||||||
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
if (asset.isVideo || asset.isMotionPhoto) {
|
if (asset.isVideo || asset.isMotionPhoto) {
|
||||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||||
@@ -184,6 +195,40 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
_delayedOperations.add(timer);
|
_delayedOperations.add(timer);
|
||||||
|
|
||||||
|
_handleCasting(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleCasting(BaseAsset asset) {
|
||||||
|
if (!ref.read(castProvider).isCasting) return;
|
||||||
|
|
||||||
|
// hide any casting snackbars if they exist
|
||||||
|
context.scaffoldMessenger.hideCurrentSnackBar();
|
||||||
|
|
||||||
|
// send image to casting if the server has it
|
||||||
|
if (asset.hasRemote) {
|
||||||
|
final remoteAsset = asset as RemoteAsset;
|
||||||
|
|
||||||
|
ref.read(castProvider.notifier).loadMedia(remoteAsset, false);
|
||||||
|
} else {
|
||||||
|
// casting cannot show local assets
|
||||||
|
context.scaffoldMessenger.clearSnackBars();
|
||||||
|
|
||||||
|
if (ref.read(castProvider).isCasting) {
|
||||||
|
ref.read(castProvider.notifier).stop();
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
content: Text(
|
||||||
|
"local_asset_cast_failed".tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPageBuild(PhotoViewControllerBase controller) {
|
void _onPageBuild(PhotoViewControllerBase controller) {
|
||||||
@@ -452,7 +497,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
ImageChunkEvent? progress,
|
ImageChunkEvent? progress,
|
||||||
int index,
|
int index,
|
||||||
) {
|
) {
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
|
||||||
|
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||||
|
asset = stackChildren
|
||||||
|
.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
@@ -480,9 +530,14 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||||
scaffoldContext ??= ctx;
|
scaffoldContext ??= ctx;
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
|
||||||
|
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||||
|
asset = stackChildren
|
||||||
|
.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
||||||
if (asset.isImage && !isPlayingMotionVideo) {
|
if (asset.isImage && !isPlayingMotionVideo) {
|
||||||
return _imageBuilder(ctx, asset);
|
return _imageBuilder(ctx, asset);
|
||||||
}
|
}
|
||||||
@@ -568,8 +623,24 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||||
ref.watch(isPlayingMotionVideoProvider);
|
ref.watch(isPlayingMotionVideoProvider);
|
||||||
|
|
||||||
|
// Listen for casting changes and send initial asset to the cast provider
|
||||||
|
ref.listen(castProvider.select((value) => value.isCasting),
|
||||||
|
(_, isCasting) async {
|
||||||
|
if (!isCasting) return;
|
||||||
|
|
||||||
|
final asset = ref.read(currentAssetNotifier);
|
||||||
|
if (asset == null) return;
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_handleCasting(asset);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
|
||||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
@@ -596,7 +667,17 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
),
|
),
|
||||||
bottomNavigationBar: const ViewerBottomBar(),
|
bottomNavigationBar: showingBottomSheet
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const AssetStackRow(),
|
||||||
|
if (!isInLockedView) const ViewerBottomBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,40 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
class ViewerOpenBottomSheetEvent extends Event {
|
||||||
|
const ViewerOpenBottomSheetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class AssetViewerState {
|
class AssetViewerState {
|
||||||
final int backgroundOpacity;
|
final int backgroundOpacity;
|
||||||
final bool showingBottomSheet;
|
final bool showingBottomSheet;
|
||||||
final bool showingControls;
|
final bool showingControls;
|
||||||
|
final BaseAsset? currentAsset;
|
||||||
|
final int stackIndex;
|
||||||
|
|
||||||
const AssetViewerState({
|
const AssetViewerState({
|
||||||
this.backgroundOpacity = 255,
|
this.backgroundOpacity = 255,
|
||||||
this.showingBottomSheet = false,
|
this.showingBottomSheet = false,
|
||||||
this.showingControls = true,
|
this.showingControls = true,
|
||||||
|
this.currentAsset,
|
||||||
|
this.stackIndex = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
AssetViewerState copyWith({
|
AssetViewerState copyWith({
|
||||||
int? backgroundOpacity,
|
int? backgroundOpacity,
|
||||||
bool? showingBottomSheet,
|
bool? showingBottomSheet,
|
||||||
bool? showingControls,
|
bool? showingControls,
|
||||||
|
BaseAsset? currentAsset,
|
||||||
|
int? stackIndex,
|
||||||
}) {
|
}) {
|
||||||
return AssetViewerState(
|
return AssetViewerState(
|
||||||
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
|
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
|
||||||
showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet,
|
showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet,
|
||||||
showingControls: showingControls ?? this.showingControls,
|
showingControls: showingControls ?? this.showingControls,
|
||||||
|
currentAsset: currentAsset ?? this.currentAsset,
|
||||||
|
stackIndex: stackIndex ?? this.stackIndex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,14 +50,18 @@ class AssetViewerState {
|
|||||||
return other is AssetViewerState &&
|
return other is AssetViewerState &&
|
||||||
other.backgroundOpacity == backgroundOpacity &&
|
other.backgroundOpacity == backgroundOpacity &&
|
||||||
other.showingBottomSheet == showingBottomSheet &&
|
other.showingBottomSheet == showingBottomSheet &&
|
||||||
other.showingControls == showingControls;
|
other.showingControls == showingControls &&
|
||||||
|
other.currentAsset == currentAsset &&
|
||||||
|
other.stackIndex == stackIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
backgroundOpacity.hashCode ^
|
backgroundOpacity.hashCode ^
|
||||||
showingBottomSheet.hashCode ^
|
showingBottomSheet.hashCode ^
|
||||||
showingControls.hashCode;
|
showingControls.hashCode ^
|
||||||
|
currentAsset.hashCode ^
|
||||||
|
stackIndex.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
||||||
@@ -52,6 +70,10 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
|||||||
return const AssetViewerState();
|
return const AssetViewerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAsset(BaseAsset? asset) {
|
||||||
|
state = state.copyWith(currentAsset: asset, stackIndex: 0);
|
||||||
|
}
|
||||||
|
|
||||||
void setOpacity(int opacity) {
|
void setOpacity(int opacity) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backgroundOpacity: opacity,
|
backgroundOpacity: opacity,
|
||||||
@@ -76,6 +98,10 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
|||||||
void toggleControls() {
|
void toggleControls() {
|
||||||
state = state.copyWith(showingControls: !state.showingControls);
|
state = state.copyWith(showingControls: !state.showingControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setStackIndex(int index) {
|
||||||
|
state = state.copyWith(stackIndex: index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetViewerProvider =
|
final assetViewerProvider =
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
@@ -38,8 +36,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
const _EditActionButton(),
|
|
||||||
if (asset.hasRemote && isOwner)
|
if (asset.hasRemote && isOwner)
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
];
|
];
|
||||||
@@ -86,15 +83,3 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditActionButton extends ConsumerWidget {
|
|
||||||
const _EditActionButton();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.tune_outlined,
|
|
||||||
label: 'edit'.t(context: context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
@@ -44,8 +45,10 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
if (asset.hasRemote) ...[
|
if (asset.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.viewer),
|
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
@@ -63,8 +66,10 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final lockedViewActions = <Widget>[];
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
actions: actions,
|
actions: isInLockedView ? lockedViewActions : actions,
|
||||||
slivers: const [_AssetDetailBottomSheet()],
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
initialChildSize: initialChildSize,
|
initialChildSize: initialChildSize,
|
||||||
@@ -73,6 +78,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
expand: false,
|
expand: false,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
resizeOnScroll: false,
|
resizeOnScroll: false,
|
||||||
|
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,14 +90,18 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
final dateTime = asset.createdAt.toLocal();
|
final dateTime = asset.createdAt.toLocal();
|
||||||
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
|
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
|
||||||
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
|
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
|
||||||
return '$date$_kSeparator$time';
|
final timezone = dateTime.timeZoneOffset.isNegative
|
||||||
|
? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'
|
||||||
|
: 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}';
|
||||||
|
return '$date$_kSeparator$time $timezone';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
|
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
|
||||||
final height = asset.height ?? exifInfo?.height;
|
final height = asset.height ?? exifInfo?.height;
|
||||||
final width = asset.width ?? exifInfo?.width;
|
final width = asset.width ?? exifInfo?.width;
|
||||||
final resolution =
|
final resolution = (width != null && height != null)
|
||||||
(width != null && height != null) ? "$width x $height" : null;
|
? "${width.toInt()} x ${height.toInt()}"
|
||||||
|
: null;
|
||||||
final fileSize =
|
final fileSize =
|
||||||
exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
|
exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
|
||||||
|
|
||||||
@@ -150,46 +160,46 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
// Asset Date and Time
|
// Asset Date and Time
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
titleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 16,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SheetLocationDetails(),
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: 'exif_bottom_sheet_details'.t(context: context),
|
title: 'exif_bottom_sheet_details'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelLarge,
|
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// File info
|
// File info
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: asset.name,
|
title: asset.name,
|
||||||
titleStyle: context.textTheme.labelLarge
|
titleStyle: context.textTheme.labelLarge,
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
asset.isImage ? Icons.image_outlined : Icons.videocam_outlined,
|
asset.isImage ? Icons.image_outlined : Icons.videocam_outlined,
|
||||||
size: 30,
|
size: 24,
|
||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getFileInfo(asset, exifInfo),
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||||
color: context.textTheme.labelLarge?.color?.withAlpha(200),
|
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Camera info
|
// Camera info
|
||||||
if (cameraTitle != null)
|
if (cameraTitle != null)
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: cameraTitle,
|
title: cameraTitle,
|
||||||
titleStyle: context.textTheme.labelLarge
|
titleStyle: context.textTheme.labelLarge,
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.camera_outlined,
|
Icons.camera_outlined,
|
||||||
size: 30,
|
size: 24,
|
||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||||
color: context.textTheme.labelLarge?.color?.withAlpha(200),
|
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
|
|
||||||
// Guard no lat/lng
|
// Guard no lat/lng
|
||||||
if (!hasCoordinates ||
|
if (!hasCoordinates ||
|
||||||
(asset is LocalAsset && !(asset as LocalAsset).hasRemote)) {
|
(asset != null && asset is LocalAsset && asset!.hasRemote)) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,10 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
"exif_bottom_sheet_location".t(context: context),
|
"exif_bottom_sheet_location".t(context: context),
|
||||||
style: context.textTheme.labelLarge,
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ExifMap(
|
ExifMap(
|
||||||
@@ -109,15 +112,13 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
locationName,
|
locationName,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.labelLarge,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
coordinates,
|
coordinates,
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.labelLarge?.color?.withAlpha(150),
|
color: context.textTheme.labelMedium?.color?.withAlpha(150),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
|
|
||||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
const ViewerTopAppBar({super.key});
|
const ViewerTopAppBar({super.key});
|
||||||
@@ -24,6 +28,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
|
||||||
final isShowingSheet = ref
|
final isShowingSheet = ref
|
||||||
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
@@ -37,7 +42,17 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
opacity = 0;
|
opacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isCasting = ref.watch(
|
||||||
|
castProvider.select((c) => c.isCasting),
|
||||||
|
);
|
||||||
|
final websocketConnected =
|
||||||
|
ref.watch(websocketProvider.select((c) => c.isConnected));
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
|
if (isCasting || (asset.hasRemote && websocketConnected))
|
||||||
|
const CastActionButton(
|
||||||
|
menuItem: true,
|
||||||
|
),
|
||||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||||
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
@@ -49,6 +64,14 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
const _KebabMenu(),
|
const _KebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final lockedViewActions = <Widget>[
|
||||||
|
if (isCasting || (asset.hasRemote && websocketConnected))
|
||||||
|
const CastActionButton(
|
||||||
|
menuItem: true,
|
||||||
|
),
|
||||||
|
const _KebabMenu(),
|
||||||
|
];
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: opacity < 255,
|
ignoring: opacity < 255,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
@@ -61,7 +84,11 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
iconTheme: const IconThemeData(size: 22, color: Colors.white),
|
iconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
|
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
shape: const Border(),
|
shape: const Border(),
|
||||||
actions: isShowingSheet ? null : actions,
|
actions: isShowingSheet
|
||||||
|
? null
|
||||||
|
: isInLockedView
|
||||||
|
? lockedViewActions
|
||||||
|
: actions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
maxChildSize: 0.4,
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||||
@@ -49,7 +49,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
),
|
),
|
||||||
const StackActionButton(),
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||||||
final bool expand;
|
final bool expand;
|
||||||
final bool shouldCloseOnMinExtent;
|
final bool shouldCloseOnMinExtent;
|
||||||
final bool resizeOnScroll;
|
final bool resizeOnScroll;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const BaseBottomSheet({
|
const BaseBottomSheet({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -26,6 +27,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||||||
this.expand = true,
|
this.expand = true,
|
||||||
this.shouldCloseOnMinExtent = true,
|
this.shouldCloseOnMinExtent = true,
|
||||||
this.resizeOnScroll = true,
|
this.resizeOnScroll = true,
|
||||||
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -69,8 +71,8 @@ class _BaseDraggableScrollableSheetState
|
|||||||
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
|
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
|
||||||
builder: (BuildContext context, ScrollController scrollController) {
|
builder: (BuildContext context, ScrollController scrollController) {
|
||||||
return Card(
|
return Card(
|
||||||
color: context.colorScheme.surfaceContainerHigh,
|
color: widget.backgroundColor ??
|
||||||
surfaceTintColor: context.colorScheme.surfaceContainerHigh,
|
context.colorScheme.surfaceContainerHigh,
|
||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
elevation: 6.0,
|
elevation: 6.0,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user