mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 15:42:32 -04:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04e179c7d6 | |||
| 398f613724 | |||
| 54d7b0feb9 | |||
| 05334569e3 | |||
| c72ad193fa | |||
| 6f8fb9e42b | |||
| 746599319b | |||
| 62578cfad5 | |||
| 4d886ba0ad | |||
| 3005ff0cc4 | |||
| 9ea4a03f21 | |||
| 69143a53b7 | |||
| 86255f8c31 | |||
| b3b651aec0 | |||
| 5e4b64670c | |||
| dd7a51a2d9 | |||
| 646b8249ca | |||
| 352c129d92 | |||
| 3ac91797e8 | |||
| a7d634bacd | |||
| 11cf9ffd85 | |||
| 220891d533 | |||
| d4808fdc4d | |||
| 7fa967a98e | |||
| 9cffcc9f4e | |||
| 40925f0a06 | |||
| 0544d22902 | |||
| 3d075f2bf8 | |||
| 7384799f19 | |||
| 4a7f06e8fd | |||
| 8f662fc459 | |||
| 24b1dae9f2 | |||
| 3a3469a5f9 | |||
| 7993619ed2 | |||
| 4d1f6f869b | |||
| 3eb03f7934 | |||
| 03ed3daa31 | |||
| 02581e81a7 | |||
| 3ab3d5cf43 | |||
| 0ef04d9baa | |||
| df016f9228 | |||
| 17779c1e74 | |||
| 01d6a244d8 | |||
| 21d6755f39 | |||
| e91c017dd0 | |||
| 43687cd8b4 | |||
| 06729ee5a5 | |||
| b0c9743d9a | |||
| 37cc028868 | |||
| 84a2b7a3c8 | |||
| 89b3433346 | |||
| 3ff0d47ee3 | |||
| aeaf846482 | |||
| b031548791 | |||
| fcea617313 | |||
| 024f20ea26 | |||
| 0a4ed6fd71 | |||
| b6e2ce1f35 | |||
| e323e778cd | |||
| 6a87797649 | |||
| f4a4649bbc | |||
| 6ca54ee722 | |||
| 8e3035f783 | |||
| 79801595db | |||
| 3e1c8aacb1 | |||
| 91ac56cef2 | |||
| 58beac8fe0 | |||
| f632d320f5 | |||
| 2ddaf6a611 | |||
| 1932c60e1c | |||
| dc6f8e746e | |||
| ad7aedb843 | |||
| 571e6a8560 | |||
| 4791313def | |||
| f88fdae048 | |||
| bcef7aa6b6 | |||
| ce292bdce9 | |||
| 4eee023648 | |||
| 8f4b0fce49 | |||
| c6b3127b35 | |||
| 4d6a50c2cb | |||
| 15f3947ae6 | |||
| e142e3aca7 | |||
| 38438c8d9a | |||
| a278c10c75 | |||
| 2276443c56 | |||
| bb44773e57 | |||
| 14d9e90a03 | |||
| 03e042213c | |||
| db589455f4 |
@@ -75,7 +75,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Build Immich CLI",
|
"label": "Build Immich CLI",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pnpm --filter cli build:dev"
|
"command": "pnpm --filter @immich/cli build:dev"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- ../plugins:/build/corePlugin
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
cli:
|
cli:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- cli/src/**
|
- packages/cli/src/**
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Setup Mise
|
||||||
|
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||||
|
with:
|
||||||
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
env:
|
env:
|
||||||
@@ -111,16 +116,8 @@ jobs:
|
|||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
~/.android/sdk
|
~/.android/sdk
|
||||||
mobile/android/.gradle
|
mobile/android/.gradle
|
||||||
mobile/.dart_tool
|
|
||||||
key: build-mobile-gradle-${{ runner.os }}-main
|
key: build-mobile-gradle-${{ runner.os }}-main
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
|
||||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||||
with:
|
with:
|
||||||
@@ -131,11 +128,10 @@ jobs:
|
|||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: mise //mobile:codegen:translation
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Generate platform APIs
|
- name: Generate platform APIs
|
||||||
run: make pigeon
|
run: mise //mobile:codegen:pigeon
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Build Android App Bundle
|
- name: Build Android App Bundle
|
||||||
@@ -192,7 +188,6 @@ jobs:
|
|||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
~/.android/sdk
|
~/.android/sdk
|
||||||
mobile/android/.gradle
|
mobile/android/.gradle
|
||||||
mobile/.dart_tool
|
|
||||||
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
|
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
|
||||||
|
|
||||||
build-sign-ios:
|
build-sign-ios:
|
||||||
@@ -205,6 +200,12 @@ jobs:
|
|||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- id: token
|
||||||
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
|
with:
|
||||||
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Select Xcode 26
|
- name: Select Xcode 26
|
||||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||||
|
|
||||||
@@ -214,24 +215,20 @@ jobs:
|
|||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Mise
|
||||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Flutter dependencies
|
- name: Install Flutter dependencies
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Generate translation files
|
- name: Generate translation files
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: mise //mobile:codegen:translation
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Generate platform APIs
|
- name: Generate platform APIs
|
||||||
run: make pigeon
|
run: mise //mobile:codegen:pigeon
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
|
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'cli/**'
|
- 'packages/cli/**'
|
||||||
- '.github/workflows/cli.yml'
|
- '.github/workflows/cli.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'cli/**'
|
- 'packages/cli/**'
|
||||||
- '.github/workflows/cli.yml'
|
- '.github/workflows/cli.yml'
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Get package version
|
- name: Get package version
|
||||||
id: package-version
|
id: package-version
|
||||||
run: |
|
run: |
|
||||||
version=$(jq -r '.version' cli/package.json)
|
version=$(jq -r '.version' packages/cli/package.json)
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: packages/cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
needs: [get_body, should_run]
|
needs: [get_body, should_run]
|
||||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
|
image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0
|
||||||
outputs:
|
outputs:
|
||||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ jobs:
|
|||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
@@ -60,38 +60,30 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Mise
|
||||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Install dependencies for UI package
|
- name: Install dependencies for UI package
|
||||||
run: dart pub get
|
run: flutter pub get
|
||||||
working-directory: ./mobile/packages/ui
|
working-directory: ./mobile/packages/ui
|
||||||
|
|
||||||
- name: Install dependencies for UI Showcase
|
- name: Install dependencies for UI Showcase
|
||||||
run: dart pub get
|
run: flutter pub get
|
||||||
working-directory: ./mobile/packages/ui/showcase
|
working-directory: ./mobile/packages/ui/showcase
|
||||||
|
|
||||||
- name: Install DCM
|
- name: Generate translation files
|
||||||
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
run: mise //mobile:codegen:translation
|
||||||
with:
|
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
|
||||||
version: auto
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Generate translation file
|
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
|
||||||
|
|
||||||
- name: Run Build Runner
|
- name: Run Build Runner
|
||||||
run: make build
|
run: mise //mobile:codegen:dart
|
||||||
|
|
||||||
- name: Generate platform API
|
- name: Generate platform API
|
||||||
run: make pigeon
|
run: mise //mobile:codegen:pigeon
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -107,20 +99,16 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory"
|
echo "ERROR: Generated files not up to date! Run 'mise //mobile:codegen:dart' and 'mise //mobile:codegen:pigeon'"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run dart analyze
|
- name: Run analyze
|
||||||
run: dart analyze --fatal-infos
|
run: mise //mobile:analyze
|
||||||
|
|
||||||
- name: Run dart format
|
- name: Run format
|
||||||
run: make format
|
run: mise //mobile:format
|
||||||
|
|
||||||
# TODO: Re-enable after upgrading custom_lint
|
# TODO: Re-enable after upgrading custom_lint
|
||||||
# - name: Run dart custom_lint
|
# - name: Run dart custom_lint
|
||||||
# run: dart run custom_lint
|
# run: dart run custom_lint
|
||||||
|
|
||||||
# TODO: Use https://github.com/CQLabs/dcm-action
|
|
||||||
- name: Run DCM
|
|
||||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
|
||||||
|
|||||||
+31
-38
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- 'server/**'
|
- 'server/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
cli:
|
cli:
|
||||||
- 'cli/**'
|
- 'packages/cli/**'
|
||||||
- 'packages/sdk/**'
|
- 'packages/sdk/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
e2e:
|
e2e:
|
||||||
@@ -62,9 +62,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -84,7 +81,7 @@ jobs:
|
|||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Run ci-unit
|
- name: Run ci-unit
|
||||||
run: mise run ci-unit
|
run: mise run //server:ci-unit
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Unit Test CLI
|
name: Unit Test CLI
|
||||||
@@ -95,7 +92,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -126,7 +123,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./cli
|
working-directory: ./packages/cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -256,10 +253,10 @@ jobs:
|
|||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm --filter=immich-i18n install --frozen-lockfile
|
run: pnpm -w install --frozen-lockfile
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: pnpm --filter=immich-i18n format:fix
|
run: pnpm format:fix
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -379,19 +376,14 @@ jobs:
|
|||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup @immich/sdk
|
- name: Setup packages
|
||||||
run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
|
run: pnpm --filter @immich/sdk --filter @immich/cli install --frozen-lockfile && pnpm --filter @immich/sdk --filter @immich/cli build
|
||||||
|
|
||||||
- name: Run setup web
|
- name: Run setup web
|
||||||
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run setup cli
|
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
|
||||||
working-directory: ./cli
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
@@ -556,17 +548,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Setup Flutter SDK
|
|
||||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
- name: Setup Mise
|
||||||
|
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
|
||||||
- name: Generate translation file
|
- name: Install dependencies
|
||||||
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
|
run: flutter pub get
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
|
- name: Generate translation files
|
||||||
|
run: mise //mobile:codegen:translation
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
run: mise //mobile:test
|
||||||
run: flutter test -j 1
|
|
||||||
ml-unit-tests:
|
ml-unit-tests:
|
||||||
name: Unit Test ML
|
name: Unit Test ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -678,12 +675,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
run: pnpm --filter immich build
|
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: ./bin/generate-open-api.sh
|
run: mise //:open-api
|
||||||
working-directory: open-api
|
working-directory: open-api
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
@@ -720,9 +713,6 @@ jobs:
|
|||||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -744,18 +734,21 @@ jobs:
|
|||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build plugins
|
||||||
|
run: mise //:plugins
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: mise //server:build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: pnpm migrations:run
|
run: pnpm --filter immich migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: pnpm schema:reset
|
run: pnpm --filter immich schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: pnpm migrations:generate src/TestMigration
|
run: pnpm --filter migrations:generate src/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -771,11 +764,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated migration files not up to date!"
|
echo "ERROR: Generated migration files not up to date!"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
cat ./src/*-TestMigration.ts
|
cat ./server/src/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: pnpm sync:sql
|
run: mise //:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
i18n:
|
i18n:
|
||||||
- modified: 'i18n/!(en|package)**\.json'
|
- modified: 'i18n/!(en)**\.json'
|
||||||
skip-force-logic: 'true'
|
skip-force-logic: 'true'
|
||||||
|
|
||||||
enforce-lock:
|
enforce-lock:
|
||||||
|
|||||||
Vendored
+6
-4
@@ -23,15 +23,17 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Immich CLI",
|
"name": "Immich CLI",
|
||||||
"program": "${workspaceFolder}/cli/dist/index.js",
|
"program": "${workspaceFolder}/packages/cli/dist/index.js",
|
||||||
"args": ["upload", "--help"],
|
"args": ["upload", "--help"],
|
||||||
"runtimeArgs": ["--enable-source-maps"],
|
"runtimeArgs": ["--enable-source-maps"],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"resolveSourceMapLocations": ["${workspaceFolder}/cli/dist/**/*.js.map"],
|
"resolveSourceMapLocations": [
|
||||||
|
"${workspaceFolder}/packages/cli/dist/**/*.js.map"
|
||||||
|
],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"outFiles": ["${workspaceFolder}/cli/dist/**/*.js"],
|
"outFiles": ["${workspaceFolder}/packages/cli/dist/**/*.js"],
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"preLaunchTask": "Build Immich CLI"
|
"preLaunchTask": "Build @immich/cli"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,105 +37,24 @@ prod-scale:
|
|||||||
|
|
||||||
.PHONY: open-api
|
.PHONY: open-api
|
||||||
open-api:
|
open-api:
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh
|
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
open-api-dart:
|
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh dart
|
|
||||||
|
|
||||||
open-api-typescript:
|
|
||||||
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
pnpm --filter immich run sync:sql
|
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
attach-server:
|
|
||||||
docker exec -it docker_immich-server_1 sh
|
|
||||||
|
|
||||||
renovate:
|
renovate:
|
||||||
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
|
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
|
||||||
|
|
||||||
# Directories that need to be created for volumes or build output
|
|
||||||
VOLUME_DIRS = \
|
|
||||||
./.pnpm-store \
|
|
||||||
./web/.svelte-kit \
|
|
||||||
./web/node_modules \
|
|
||||||
./web/coverage \
|
|
||||||
./e2e/node_modules \
|
|
||||||
./docs/node_modules \
|
|
||||||
./server/node_modules \
|
|
||||||
./packages/sdk/node_modules \
|
|
||||||
./.github/node_modules \
|
|
||||||
./node_modules \
|
|
||||||
./cli/node_modules
|
|
||||||
|
|
||||||
# Include .env file if it exists
|
# Include .env file if it exists
|
||||||
-include docker/.env
|
-include docker/.env
|
||||||
|
|
||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
# directory to package name mapping function
|
|
||||||
# cli = @immich/cli
|
|
||||||
# docs = documentation
|
|
||||||
# e2e = immich-e2e
|
|
||||||
# packages/sdk = @immich/sdk
|
|
||||||
# server = immich
|
|
||||||
# web = immich-web
|
|
||||||
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
|
|
||||||
|
|
||||||
audit-%:
|
|
||||||
pnpm --filter $(call map-package,$*) audit fix
|
|
||||||
install-%:
|
|
||||||
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
|
|
||||||
build-cli: build-sdk
|
|
||||||
build-web: build-sdk
|
|
||||||
build-%: install-%
|
|
||||||
pnpm --filter $(call map-package,$*) run build
|
|
||||||
format-%:
|
|
||||||
pnpm --filter $(call map-package,$*) run format:fix
|
|
||||||
lint-%:
|
|
||||||
pnpm --filter $(call map-package,$*) run lint:fix
|
|
||||||
check-%:
|
|
||||||
pnpm --filter $(call map-package,$*) run check
|
|
||||||
check-web:
|
|
||||||
pnpm --filter immich-web run check:typescript
|
|
||||||
pnpm --filter immich-web run check:svelte
|
|
||||||
test-%:
|
|
||||||
pnpm --filter $(call map-package,$*) run test
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
pnpm --filter immich-e2e run test
|
pnpm --filter immich-e2e run test
|
||||||
pnpm --filter immich-e2e run test:web
|
pnpm --filter immich-e2e run test:web
|
||||||
test-medium:
|
|
||||||
docker run \
|
|
||||||
--rm \
|
|
||||||
-v ./server/src:/usr/src/app/src \
|
|
||||||
-v ./server/test:/usr/src/app/test \
|
|
||||||
-v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
|
|
||||||
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
|
||||||
-e NODE_ENV=development \
|
|
||||||
immich-server:latest \
|
|
||||||
-c "pnpm test:medium -- --run"
|
|
||||||
test-medium-dev:
|
|
||||||
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
|
|
||||||
|
|
||||||
install-all:
|
|
||||||
pnpm -r --filter '!documentation' install
|
|
||||||
|
|
||||||
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
|
|
||||||
|
|
||||||
check-all:
|
|
||||||
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
|
|
||||||
lint-all:
|
|
||||||
pnpm -r --filter '!documentation' run lint:fix
|
|
||||||
format-all:
|
|
||||||
pnpm -r --filter '!documentation' run format:fix
|
|
||||||
audit-all:
|
|
||||||
pnpm -r --filter '!documentation' audit fix
|
|
||||||
hygiene-all: audit-all
|
|
||||||
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
|
|
||||||
|
|
||||||
test-all:
|
|
||||||
pnpm -r --filter '!documentation' run "/^test/"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||||
@@ -146,7 +65,3 @@ clean:
|
|||||||
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||||
|
|
||||||
|
|
||||||
setup-server-dev: install-server
|
|
||||||
setup-web-dev: install-sdk build-sdk install-web
|
|
||||||
|
|||||||
+30
-30
@@ -2,37 +2,37 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.5"
|
version = "4.52.7"
|
||||||
constraints = "4.52.5"
|
constraints = "4.52.7"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=",
|
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
||||||
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=",
|
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
||||||
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=",
|
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
||||||
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=",
|
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
||||||
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=",
|
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
||||||
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=",
|
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
||||||
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=",
|
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
||||||
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=",
|
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
||||||
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=",
|
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
||||||
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=",
|
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
||||||
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=",
|
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
||||||
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=",
|
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
||||||
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=",
|
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
||||||
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=",
|
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
||||||
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b",
|
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
||||||
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a",
|
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
||||||
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7",
|
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
||||||
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238",
|
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
||||||
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b",
|
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
||||||
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
|
|
||||||
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
|
|
||||||
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54",
|
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
||||||
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852",
|
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
||||||
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0",
|
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
||||||
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5",
|
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
||||||
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036",
|
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
||||||
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803",
|
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
||||||
|
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
||||||
|
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
||||||
|
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.5"
|
version = "4.52.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-30
@@ -2,37 +2,37 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.52.5"
|
version = "4.52.7"
|
||||||
constraints = "4.52.5"
|
constraints = "4.52.7"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=",
|
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
|
||||||
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=",
|
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
|
||||||
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=",
|
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
|
||||||
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=",
|
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
|
||||||
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=",
|
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
|
||||||
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=",
|
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
|
||||||
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=",
|
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
|
||||||
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=",
|
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
|
||||||
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=",
|
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
|
||||||
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=",
|
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
|
||||||
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=",
|
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
|
||||||
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=",
|
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
|
||||||
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=",
|
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
|
||||||
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=",
|
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
|
||||||
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b",
|
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
|
||||||
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a",
|
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
|
||||||
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7",
|
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
|
||||||
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238",
|
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
|
||||||
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b",
|
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
|
||||||
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
|
|
||||||
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
|
|
||||||
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54",
|
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
|
||||||
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852",
|
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
|
||||||
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0",
|
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
|
||||||
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5",
|
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
|
||||||
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036",
|
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
|
||||||
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803",
|
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
|
||||||
|
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
|
||||||
|
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
|
||||||
|
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.52.5"
|
version = "4.52.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
- server_node_modules:/usr/src/app/server/node_modules
|
- server_node_modules:/usr/src/app/server/node_modules
|
||||||
- web_node_modules:/usr/src/app/web/node_modules
|
- web_node_modules:/usr/src/app/web/node_modules
|
||||||
- github_node_modules:/usr/src/app/.github/node_modules
|
- github_node_modules:/usr/src/app/.github/node_modules
|
||||||
- cli_node_modules:/usr/src/app/cli/node_modules
|
- cli_node_modules:/usr/src/app/packages/cli/node_modules
|
||||||
- docs_node_modules:/usr/src/app/docs/node_modules
|
- docs_node_modules:/usr/src/app/docs/node_modules
|
||||||
- e2e_node_modules:/usr/src/app/e2e/node_modules
|
- e2e_node_modules:/usr/src/app/e2e/node_modules
|
||||||
- sdk_node_modules:/usr/src/app/packages/sdk/node_modules
|
- sdk_node_modules:/usr/src/app/packages/sdk/node_modules
|
||||||
@@ -74,7 +74,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos:/data
|
- ${UPLOAD_LOCATION}/photos:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- ../plugins:/build/corePlugin
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -157,7 +157,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
user: '1000:1000'
|
user: '1000:1000'
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
|
|||||||
| `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 |
|
||||||
|
| `grant-admin` | Grant admin privileges to a user (by email) |
|
||||||
|
| `revoke-admin` | Revoke admin privileges from a user (by email) |
|
||||||
| `version` | Print Immich version |
|
| `version` | Print Immich version |
|
||||||
| `change-media-location` | Change database file paths to align with a new media location |
|
| `change-media-location` | Change database file paths to align with a new media location |
|
||||||
|
| `schema-check` | Verify database migrations and check for schema drift |
|
||||||
|
|
||||||
## How to run a command
|
## How to run a command
|
||||||
|
|
||||||
@@ -102,6 +105,22 @@ immich-admin list-users
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Grant Admin
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin grant-admin
|
||||||
|
? Please enter the user email: user@example.com
|
||||||
|
Admin access has been granted to user@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Revoke Admin
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin revoke-admin
|
||||||
|
? Please enter the user email: user@example.com
|
||||||
|
Admin access has been revoked from user@example.com
|
||||||
|
```
|
||||||
|
|
||||||
Print Immich Version
|
Print Immich Version
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -126,3 +145,12 @@ immich-admin change-media-location
|
|||||||
Database file paths updated successfully! 🎉
|
Database file paths updated successfully! 🎉
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Schema Check
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin schema-check
|
||||||
|
Migrations are up to date
|
||||||
|
|
||||||
|
No schema drift detected
|
||||||
|
```
|
||||||
|
|||||||
@@ -243,8 +243,8 @@ To connect the mobile app to your Dev Container:
|
|||||||
|
|
||||||
- **Server code** (`/server`): Changes trigger automatic restart
|
- **Server code** (`/server`): Changes trigger automatic restart
|
||||||
- **Web code** (`/web`): Changes trigger hot module replacement
|
- **Web code** (`/web`): Changes trigger hot module replacement
|
||||||
- **Database migrations**: Run `pnpm run sync:sql` in the server directory
|
- **Database migrations**: Run `mise //:sql`
|
||||||
- **API changes**: Regenerate TypeScript SDK with `make open-api`
|
- **API changes**: Regenerate TypeScript SDK with `mise //:open-api`
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -252,20 +252,11 @@ To connect the mobile app to your Dev Container:
|
|||||||
|
|
||||||
The Dev Container supports multiple ways to run tests:
|
The Dev Container supports multiple ways to run tests:
|
||||||
|
|
||||||
#### Using Make Commands (Recommended)
|
#### Using Mise Commands (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests for specific components
|
# Run tests for specific components
|
||||||
make test-server # Server unit tests
|
mise run checklist # in `server/`, `web/`, `packages/cli`
|
||||||
make test-web # Web unit tests
|
|
||||||
make test-e2e # End-to-end tests
|
|
||||||
make test-cli # CLI tests
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
make test-all # Runs tests for all components
|
|
||||||
|
|
||||||
# Medium tests (integration tests)
|
|
||||||
make test-medium-dev # End-to-end tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using PNPM Directly
|
#### Using PNPM Directly
|
||||||
@@ -289,48 +280,16 @@ pnpm run test # Run API tests
|
|||||||
pnpm run test:web # Run web UI tests
|
pnpm run test:web # Run web UI tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code Quality Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linting
|
|
||||||
make lint-server # Lint server code
|
|
||||||
make lint-web # Lint web code
|
|
||||||
make lint-all # Lint all components
|
|
||||||
|
|
||||||
# Formatting
|
|
||||||
make format-server # Format server code
|
|
||||||
make format-web # Format web code
|
|
||||||
make format-all # Format all code
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
make check-server # Type check server
|
|
||||||
make check-web # Type check web
|
|
||||||
make check-all # Check all components
|
|
||||||
|
|
||||||
# Complete hygiene check
|
|
||||||
make hygiene-all # Run lint, format, check, SQL sync, and audit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Additional Make Commands
|
### Additional Make Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build commands
|
|
||||||
make build-server # Build server
|
|
||||||
make build-web # Build web app
|
|
||||||
make build-all # Build everything
|
|
||||||
|
|
||||||
# API generation
|
# API generation
|
||||||
make open-api # Generate OpenAPI specs
|
make open-api # Generate OpenAPI specs
|
||||||
make open-api-typescript # Generate TypeScript SDK
|
make open-api-typescript # Generate TypeScript SDK
|
||||||
make open-api-dart # Generate Dart SDK
|
make open-api-dart # Generate Dart SDK
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
make sql # Sync database schema
|
mise sql # Sync database schema
|
||||||
|
|
||||||
# Dependencies
|
|
||||||
make install-server # Install server dependencies
|
|
||||||
make install-web # Install web dependencies
|
|
||||||
make install-all # Install all dependencies
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debugging
|
### Debugging
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
|
|||||||
| :------------------ | :------------------------------------------------------------------- |
|
| :------------------ | :------------------------------------------------------------------- |
|
||||||
| `.github/` | Github templates and action workflows |
|
| `.github/` | Github templates and action workflows |
|
||||||
| `.vscode/` | VSCode debug launch profiles |
|
| `.vscode/` | VSCode debug launch profiles |
|
||||||
| `cli/` | Source code for the work-in-progress CLI rewrite |
|
| `packages/cli` | Source code for the CLI |
|
||||||
|
| `packages/sdk` | Source code for the generated OpenAPI SDK |
|
||||||
| `docker/` | Docker compose resources for dev, test, production |
|
| `docker/` | Docker compose resources for dev, test, production |
|
||||||
| `design/` | Screenshots and logos for the README |
|
| `design/` | Screenshots and logos for the README |
|
||||||
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
|
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
|
||||||
|
|||||||
@@ -34,21 +34,23 @@ Run all web checks with `pnpm run check:all`
|
|||||||
Run all server checks with `pnpm run check:all`
|
Run all server checks with `pnpm run check:all`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info Auto Fix
|
:::tip Auto Fix
|
||||||
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
|
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Mobile Checks
|
## Mobile Checklist
|
||||||
|
|
||||||
The following commands must be executed from within the mobile app directory of the codebase.
|
- [ ] `mise //mobile:codegen` (auto-generate files using build_runner)
|
||||||
|
- [ ] `mise //mobile:lint` (static analysis via Dart Analyzer and DCM)
|
||||||
|
- [ ] `mise //mobile:format` (formatting via Dart Formatter)
|
||||||
|
- [ ] `mise //mobile:test` (unit tests)
|
||||||
|
|
||||||
- [ ] `make build` (auto-generate files using build_runner)
|
:::tip
|
||||||
- [ ] `make analyze` (static analysis via Dart Analyzer and DCM)
|
Run all these commands at once with `mise //mobile:checklist`
|
||||||
- [ ] `make format` (formatting via Dart Formatter)
|
:::
|
||||||
- [ ] `make test` (unit tests)
|
|
||||||
|
|
||||||
:::info Auto Fix
|
:::tip Auto Fix
|
||||||
You can use `dart fix --apply` and `dcm fix lib` to potentially correct some issues automatically for `make analyze`.
|
You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## OpenAPI
|
## OpenAPI
|
||||||
|
|||||||
@@ -17,15 +17,14 @@ make e2e
|
|||||||
|
|
||||||
Before you can run the tests, you need to run the following commands _once_:
|
Before you can run the tests, you need to run the following commands _once_:
|
||||||
|
|
||||||
- `pnpm install` (in `e2e/`)
|
- `pnpm install`
|
||||||
- `pnpm run build` (in `cli/`)
|
- `pnpm --filter @immich/sdk --filter @immich/cli build`
|
||||||
- `make open-api` (in the project root `/`)
|
- `mise //:open-api`
|
||||||
|
|
||||||
Once the test environment is running, the e2e tests can be run via:
|
Once the test environment is running, the e2e tests can be run via:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd e2e/
|
mise //e2e:test
|
||||||
pnpm test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The tests check various things including:
|
The tests check various things including:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Scroll to the bottom of the "**Details**" section and find the `IP Address` list
|
|||||||
|
|
||||||
## Step 4 - Configure Firewall Settings
|
## Step 4 - Configure Firewall Settings
|
||||||
|
|
||||||
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
|
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS to allow communication between the Immich containers.
|
||||||
|
|
||||||
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
|
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instr
|
|||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Updating Immich using Container Manager</summary>
|
<summary>Updating Immich using Container Manager</summary>
|
||||||
|
|
||||||
Check the post installation and upgrade instructions at the links above before proceeding with this section.
|
Check the post installation and upgrade instructions at the links above before proceeding with this section.
|
||||||
|
|
||||||
## Step 1. Backup
|
## Step 1. Backup
|
||||||
@@ -110,7 +111,7 @@ Go to **Project**, select **Action** then **Build**. This will download, unpack,
|
|||||||
|
|
||||||
## Step 5. Update firewall rule
|
## Step 5. Update firewall rule
|
||||||
|
|
||||||
The default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address.
|
Without a fixed subnet, the default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address.
|
||||||
|
|
||||||
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
|
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
|
||||||

|

|
||||||
@@ -123,4 +124,67 @@ In this example, the IP addresses mismatch and the firewall rule needs to be edi
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
To prevent future firewall issues, you may set a fixed subnet. [See Set Fixed Subnet](#set-fixed-subnet) for instructions.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details id="set-fixed-subnet">
|
||||||
|
<summary>Set Fixed Subnet</summary>
|
||||||
|
|
||||||
|
Docker by default assigns dynamic subnets to bridge networks which can change when rebuilding containers and can cause firewall rules to break. To avoid this, define a fixed subnet in your `docker-compose.yml`:
|
||||||
|
|
||||||
|
## Step 1. Determine current subnet
|
||||||
|
|
||||||
|
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
|
||||||
|

|
||||||
|
|
||||||
|
## Step 2. Add network configuration
|
||||||
|
|
||||||
|
Add the following network configuration at the end of your `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
immich-network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
|
gateway: 172.20.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
If your docker container is running on a different subnet then update accordingly.
|
||||||
|
|
||||||
|
## Step 3. Add network to each service
|
||||||
|
|
||||||
|
Add the network to each service (immich-server, immich-machine-learning, redis, database):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
# other config options
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
# other config options
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
# other config options
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
|
||||||
|
database:
|
||||||
|
# other config options
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
```
|
||||||
|
|
||||||
|
Save your changes. Synology will ask if you want to save changes only or rebuild containers. Select rebuild containers.
|
||||||
|
|
||||||
|
## Step 4. Update Firewall Rules, if necessary
|
||||||
|
|
||||||
|
If your firewall rules were not already set for this subnet, the firewall rules will need to be updated. See [Step 4 - Configure Firewall Settings](#step-4---configure-firewall-settings).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
e2e-auth-server:
|
e2e-auth-server:
|
||||||
container_name: immich-e2e-auth-server
|
container_name: immich-e2e-auth-server
|
||||||
build:
|
build:
|
||||||
context: ../e2e-auth-server
|
context: ../packages/e2e-auth-server
|
||||||
ports:
|
ports:
|
||||||
- 2286:2286
|
- 2286:2286
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich-e2e-redis
|
container_name: immich-e2e-redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { LoginResponseDto, ManualJobName } from '@immich/sdk';
|
|||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/admin/database-backups', () => {
|
describe('/admin/database-backups', () => {
|
||||||
let cookie: string | undefined;
|
let cookie: string | undefined;
|
||||||
@@ -13,6 +13,9 @@ describe('/admin/database-backups', () => {
|
|||||||
admin = await utils.adminSetup({
|
admin = await utils.adminSetup({
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
await utils.resetBackups(admin.accessToken);
|
await utils.resetBackups(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
getMyUser,
|
getMyUser,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
updateConfig,
|
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@@ -24,7 +23,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||||
const facesAssetDir = `${testAssetDir}/metadata/faces`;
|
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
const filepath = join(tempDir, filename);
|
const filepath = join(tempDir, filename);
|
||||||
@@ -185,78 +183,6 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('faces', () => {
|
|
||||||
const metadataFaceTests = [
|
|
||||||
{
|
|
||||||
description: 'without orientation',
|
|
||||||
filename: 'portrait.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'adjusting face regions to orientation',
|
|
||||||
filename: 'portrait-orientation-6.jpg',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// should produce same resulting face region coordinates for any orientation
|
|
||||||
const expectedFaces = [
|
|
||||||
{
|
|
||||||
name: 'Marie Curie',
|
|
||||||
birthDate: null,
|
|
||||||
isHidden: false,
|
|
||||||
faces: [
|
|
||||||
{
|
|
||||||
imageHeight: 700,
|
|
||||||
imageWidth: 840,
|
|
||||||
boundingBoxX1: 261,
|
|
||||||
boundingBoxX2: 356,
|
|
||||||
boundingBoxY1: 146,
|
|
||||||
boundingBoxY2: 284,
|
|
||||||
sourceType: 'exif',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Pierre Curie',
|
|
||||||
birthDate: null,
|
|
||||||
isHidden: false,
|
|
||||||
faces: [
|
|
||||||
{
|
|
||||||
imageHeight: 700,
|
|
||||||
imageWidth: 840,
|
|
||||||
boundingBoxX1: 536,
|
|
||||||
boundingBoxX2: 618,
|
|
||||||
boundingBoxY1: 83,
|
|
||||||
boundingBoxY2: 252,
|
|
||||||
sourceType: 'exif',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
|
|
||||||
const config = await utils.getSystemConfig(admin.accessToken);
|
|
||||||
config.metadata.faces.import = true;
|
|
||||||
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
|
|
||||||
const facesAsset = await utils.createAsset(admin.accessToken, {
|
|
||||||
assetData: {
|
|
||||||
filename,
|
|
||||||
bytes: await readFile(`${facesAssetDir}/${filename}`),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/assets/${facesAsset.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body.id).toEqual(facesAsset.id);
|
|
||||||
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
|
|
||||||
expect(sortedPeople).toMatchObject(expectedFaces);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
it('should work with a shared link', async () => {
|
||||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
|
|||||||
@@ -441,7 +441,18 @@ describe('/search', () => {
|
|||||||
.get('/search/explore')
|
.get('/search/explore')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([{ fieldName: 'exifInfo.city', items: [] }]);
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body).toEqual(expect.arrayContaining([{ fieldName: 'exifInfo.city', items: [] }]));
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
fieldName: 'createdAt',
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ data: expect.objectContaining({ id: assetLast.id }) }),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { immichCli } from 'src/utils';
|
import { immichCli } from 'src/utils';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
|
const pkg = JSON.parse(readFileSync('../packages/cli/package.json', 'utf8'));
|
||||||
|
|
||||||
describe(`immich --version`, () => {
|
describe(`immich --version`, () => {
|
||||||
describe('immich --version', () => {
|
describe('immich --version', () => {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
|||||||
ownerId: [],
|
ownerId: [],
|
||||||
ratio: [],
|
ratio: [],
|
||||||
thumbhash: [],
|
thumbhash: [],
|
||||||
|
createdAt: [],
|
||||||
fileCreatedAt: [],
|
fileCreatedAt: [],
|
||||||
localOffsetHours: [],
|
localOffsetHours: [],
|
||||||
isFavorite: [],
|
isFavorite: [],
|
||||||
@@ -338,7 +339,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
|||||||
livePhotoVideoId: asset.livePhotoVideoId,
|
livePhotoVideoId: asset.livePhotoVideoId,
|
||||||
tags: [],
|
tags: [],
|
||||||
people: [],
|
people: [],
|
||||||
unassignedFaces: [],
|
|
||||||
stack: asset.stack,
|
stack: asset.stack,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
|||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
people: [],
|
people: [],
|
||||||
unassignedFaces: [],
|
|
||||||
stack: undefined,
|
stack: undefined,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
|
|||||||
+3
-1
@@ -90,7 +90,7 @@ export const tempDir = tmpdir();
|
|||||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = (args: string[]) =>
|
||||||
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../packages/cli' }).promise;
|
||||||
export const dockerExec = (args: string[]) =>
|
export const dockerExec = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
|
||||||
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
|
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
|
||||||
@@ -568,6 +568,8 @@ export const utils = {
|
|||||||
name: ManualJobName.BackupDatabase,
|
name: ManualJobName.BackupDatabase,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(accessToken, 'backupDatabase');
|
||||||
|
|
||||||
return utils.poll(
|
return utils.poll(
|
||||||
() => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`),
|
() => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`),
|
||||||
({ status, body }) => status === 200 && body.backups.length === 1,
|
({ status, body }) => status === 200 && body.backups.length === 1,
|
||||||
|
|||||||
+35
-12
@@ -22,13 +22,12 @@
|
|||||||
"add_birthday": "Add a birthday",
|
"add_birthday": "Add a birthday",
|
||||||
"add_endpoint": "Add endpoint",
|
"add_endpoint": "Add endpoint",
|
||||||
"add_exclusion_pattern": "Add exclusion pattern",
|
"add_exclusion_pattern": "Add exclusion pattern",
|
||||||
"add_filter": "Add filter",
|
|
||||||
"add_filter_description": "Click to add a filter condition",
|
|
||||||
"add_location": "Add location",
|
"add_location": "Add location",
|
||||||
"add_more_users": "Add more users",
|
"add_more_users": "Add more users",
|
||||||
"add_partner": "Add partner",
|
"add_partner": "Add partner",
|
||||||
"add_path": "Add path",
|
"add_path": "Add path",
|
||||||
"add_photos": "Add photos",
|
"add_photos": "Add photos",
|
||||||
|
"add_step": "Add step",
|
||||||
"add_tag": "Add tag",
|
"add_tag": "Add tag",
|
||||||
"add_to": "Add to…",
|
"add_to": "Add to…",
|
||||||
"add_to_album": "Add to album",
|
"add_to_album": "Add to album",
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
"add_to_shared_album": "Add to shared album",
|
"add_to_shared_album": "Add to shared album",
|
||||||
"add_upload_to_stack": "Add upload to stack",
|
"add_upload_to_stack": "Add upload to stack",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
"add_workflow_step": "Add workflow step",
|
|
||||||
"added_to_archive": "Added to archive",
|
"added_to_archive": "Added to archive",
|
||||||
"added_to_favorites": "Added to favorites",
|
"added_to_favorites": "Added to favorites",
|
||||||
"added_to_favorites_count": "Added {count, number} to favorites",
|
"added_to_favorites_count": "Added {count, number} to favorites",
|
||||||
@@ -733,6 +731,7 @@
|
|||||||
"cannot_update_the_description": "Cannot update the description",
|
"cannot_update_the_description": "Cannot update the description",
|
||||||
"cast": "Cast",
|
"cast": "Cast",
|
||||||
"cast_description": "Configure available cast destinations",
|
"cast_description": "Configure available cast destinations",
|
||||||
|
"change": "Change",
|
||||||
"change_date": "Change date",
|
"change_date": "Change date",
|
||||||
"change_description": "Change description",
|
"change_description": "Change description",
|
||||||
"change_display_order": "Change display order",
|
"change_display_order": "Change display order",
|
||||||
@@ -761,6 +760,7 @@
|
|||||||
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||||
"check_logs": "Check Logs",
|
"check_logs": "Check Logs",
|
||||||
"checksum": "Checksum",
|
"checksum": "Checksum",
|
||||||
|
"choose": "Choose",
|
||||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
"cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?",
|
"cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?",
|
||||||
@@ -778,6 +778,7 @@
|
|||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"clear_all_recent_searches": "Clear all recent searches",
|
"clear_all_recent_searches": "Clear all recent searches",
|
||||||
|
"clear_failed_count": "Clear failed ({count})",
|
||||||
"clear_file_cache": "Clear File Cache",
|
"clear_file_cache": "Clear File Cache",
|
||||||
"clear_message": "Clear message",
|
"clear_message": "Clear message",
|
||||||
"clear_value": "Clear value",
|
"clear_value": "Clear value",
|
||||||
@@ -809,6 +810,7 @@
|
|||||||
"comments_are_disabled": "Comments are disabled",
|
"comments_are_disabled": "Comments are disabled",
|
||||||
"common_create_new_album": "Create new album",
|
"common_create_new_album": "Create new album",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
"configuration": "Configuration",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"confirm_admin_password": "Confirm Admin Password",
|
"confirm_admin_password": "Confirm Admin Password",
|
||||||
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
||||||
@@ -823,6 +825,7 @@
|
|||||||
"contain": "Contain",
|
"contain": "Contain",
|
||||||
"context": "Context",
|
"context": "Context",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
|
"control_bottom_app_bar_add_tags": "Add Tags",
|
||||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
@@ -885,17 +888,16 @@
|
|||||||
"cutoff_date_description": "Keep photos from the last…",
|
"cutoff_date_description": "Keep photos from the last…",
|
||||||
"cutoff_day": "{count, plural, one {day} other {days}}",
|
"cutoff_day": "{count, plural, one {day} other {days}}",
|
||||||
"cutoff_year": "{count, plural, one {year} other {years}}",
|
"cutoff_year": "{count, plural, one {year} other {years}}",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
|
||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"dark_theme": "Switch to dark theme",
|
"dark_theme": "Switch to dark theme",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"date_after": "Date after",
|
"date_after": "Date after",
|
||||||
"date_and_time": "Date and Time",
|
"date_and_time": "Date and Time",
|
||||||
"date_before": "Date before",
|
"date_before": "Date before",
|
||||||
"date_format": "E, LLL d, y • h:mm a",
|
"date_of_birth": "Date of birth",
|
||||||
"date_of_birth_saved": "Date of birth saved successfully",
|
"date_of_birth_saved": "Date of birth saved successfully",
|
||||||
"date_range": "Date range",
|
"date_range": "Date range",
|
||||||
|
"date_time_original": "Date/Time Original",
|
||||||
"day": "Day",
|
"day": "Day",
|
||||||
"days": "Days",
|
"days": "Days",
|
||||||
"deduplicate_all": "Deduplicate All",
|
"deduplicate_all": "Deduplicate All",
|
||||||
@@ -974,6 +976,7 @@
|
|||||||
"downloading_asset_filename": "Downloading asset {filename}",
|
"downloading_asset_filename": "Downloading asset {filename}",
|
||||||
"downloading_from_icloud": "Downloading from iCloud",
|
"downloading_from_icloud": "Downloading from iCloud",
|
||||||
"downloading_media": "Downloading media",
|
"downloading_media": "Downloading media",
|
||||||
|
"drag_to_reorder": "Drag to reorder",
|
||||||
"drop_files_to_upload": "Drop files anywhere to upload",
|
"drop_files_to_upload": "Drop files anywhere to upload",
|
||||||
"duplicates": "Duplicates",
|
"duplicates": "Duplicates",
|
||||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
||||||
@@ -1076,6 +1079,7 @@
|
|||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"failed_to_remove_product_key": "Failed to remove product key",
|
||||||
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
||||||
"failed_to_stack_assets": "Failed to stack assets",
|
"failed_to_stack_assets": "Failed to stack assets",
|
||||||
|
"failed_to_tag_assets": "Failed to tag assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
"failed_to_update_notification_status": "Failed to update notification status",
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
"incorrect_email_or_password": "Incorrect email or password",
|
"incorrect_email_or_password": "Incorrect email or password",
|
||||||
@@ -1195,11 +1199,13 @@
|
|||||||
"export_as_json": "Export as JSON",
|
"export_as_json": "Export as JSON",
|
||||||
"export_database": "Export Database",
|
"export_database": "Export Database",
|
||||||
"export_database_description": "Export the SQLite database",
|
"export_database_description": "Export the SQLite database",
|
||||||
|
"exposure_time": "Exposure Time",
|
||||||
"extension": "Extension",
|
"extension": "Extension",
|
||||||
"external": "External",
|
"external": "External",
|
||||||
"external_libraries": "External Libraries",
|
"external_libraries": "External Libraries",
|
||||||
"external_network": "External network",
|
"external_network": "External network",
|
||||||
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||||
|
"f_number": "F-Number",
|
||||||
"face_unassigned": "Unassigned",
|
"face_unassigned": "Unassigned",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"failed_count": "Failed: {count}",
|
"failed_count": "Failed: {count}",
|
||||||
@@ -1217,7 +1223,6 @@
|
|||||||
"features_setting_description": "Manage the app features",
|
"features_setting_description": "Manage the app features",
|
||||||
"file_name_or_extension": "File name or extension",
|
"file_name_or_extension": "File name or extension",
|
||||||
"file_name_text": "File name",
|
"file_name_text": "File name",
|
||||||
"file_name_with_value": "File name: {file_name}",
|
|
||||||
"file_size": "File size",
|
"file_size": "File size",
|
||||||
"filename": "Filename",
|
"filename": "Filename",
|
||||||
"filetype": "Filetype",
|
"filetype": "Filetype",
|
||||||
@@ -1230,6 +1235,7 @@
|
|||||||
"find_them_fast": "Find them fast by name with search",
|
"find_them_fast": "Find them fast by name with search",
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"fix_incorrect_match": "Fix incorrect match",
|
"fix_incorrect_match": "Fix incorrect match",
|
||||||
|
"focal_length": "Focal Length",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"folder_not_found": "Folder not found",
|
"folder_not_found": "Folder not found",
|
||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
@@ -1350,6 +1356,7 @@
|
|||||||
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
|
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
|
||||||
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
|
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
|
||||||
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
||||||
|
"iso": "ISO",
|
||||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"json_editor": "JSON editor",
|
"json_editor": "JSON editor",
|
||||||
@@ -1403,6 +1410,7 @@
|
|||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
"linked_oauth_account": "Linked OAuth account",
|
"linked_oauth_account": "Linked OAuth account",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
|
"live": "Live",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loading_search_results_failed": "Loading search results failed",
|
"loading_search_results_failed": "Loading search results failed",
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
@@ -1581,9 +1589,10 @@
|
|||||||
"mobile_app": "Mobile App",
|
"mobile_app": "Mobile App",
|
||||||
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
|
"modify_date": "Modify Date",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"motion": "Motion",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"move_down": "Move down",
|
"move_down": "Move down",
|
||||||
"move_off_locked_folder": "Move out of locked folder",
|
"move_off_locked_folder": "Move out of locked folder",
|
||||||
@@ -1629,7 +1638,6 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"next_memory": "Next memory",
|
"next_memory": "Next memory",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"no_actions_added": "No actions added yet",
|
|
||||||
"no_albums_found": "No albums found",
|
"no_albums_found": "No albums found",
|
||||||
"no_albums_message": "Create an album to organize your photos and videos",
|
"no_albums_message": "Create an album to organize your photos and videos",
|
||||||
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
||||||
@@ -1646,7 +1654,6 @@
|
|||||||
"no_exif_info_available": "No exif info available",
|
"no_exif_info_available": "No exif info available",
|
||||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
"no_explore_results_message": "Upload more photos to explore your collection.",
|
||||||
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
||||||
"no_filters_added": "No filters added yet",
|
|
||||||
"no_libraries_message": "Create an external library to view your photos and videos",
|
"no_libraries_message": "Create an external library to view your photos and videos",
|
||||||
"no_local_assets_found": "No local assets found with this checksum",
|
"no_local_assets_found": "No local assets found with this checksum",
|
||||||
"no_location_set": "No location set",
|
"no_location_set": "No location set",
|
||||||
@@ -1659,6 +1666,7 @@
|
|||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
"no_results_description": "Try a synonym or more general keyword",
|
"no_results_description": "Try a synonym or more general keyword",
|
||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
|
"no_steps": "No steps added yet",
|
||||||
"no_uploads_in_progress": "No uploads in progress",
|
"no_uploads_in_progress": "No uploads in progress",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"not_allowed": "Not allowed",
|
"not_allowed": "Not allowed",
|
||||||
@@ -1704,6 +1712,7 @@
|
|||||||
"organize_into_albums": "Organize into albums",
|
"organize_into_albums": "Organize into albums",
|
||||||
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
||||||
"organize_your_library": "Organize your library",
|
"organize_your_library": "Organize your library",
|
||||||
|
"orientation": "Orientation",
|
||||||
"original": "original",
|
"original": "original",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
"other_devices": "Other devices",
|
"other_devices": "Other devices",
|
||||||
@@ -1795,6 +1804,8 @@
|
|||||||
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
||||||
"play_transcoded_video": "Play transcoded video",
|
"play_transcoded_video": "Play transcoded video",
|
||||||
"please_auth_to_access": "Please authenticate to access",
|
"please_auth_to_access": "Please authenticate to access",
|
||||||
|
"plugin_method_filter_type": "Filter",
|
||||||
|
"plugin_method_filter_type_description": "This method can filter events and conditionally prevent subsequent steps from running",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
"preferences_settings_title": "Preferences",
|
"preferences_settings_title": "Preferences",
|
||||||
@@ -1816,6 +1827,7 @@
|
|||||||
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
||||||
"profile_image_of_user": "Profile image of {user}",
|
"profile_image_of_user": "Profile image of {user}",
|
||||||
"profile_picture_set": "Profile picture set.",
|
"profile_picture_set": "Profile picture set.",
|
||||||
|
"projection_type": "Projection Type",
|
||||||
"public_album": "Public album",
|
"public_album": "Public album",
|
||||||
"public_share": "Public Share",
|
"public_share": "Public Share",
|
||||||
"purchase_account_info": "Supporter",
|
"purchase_account_info": "Supporter",
|
||||||
@@ -1893,6 +1905,7 @@
|
|||||||
"remove_assets_title": "Remove assets?",
|
"remove_assets_title": "Remove assets?",
|
||||||
"remove_custom_date_range": "Remove custom date range",
|
"remove_custom_date_range": "Remove custom date range",
|
||||||
"remove_deleted_assets": "Remove Deleted Assets",
|
"remove_deleted_assets": "Remove Deleted Assets",
|
||||||
|
"remove_filter": "Remove filter",
|
||||||
"remove_from_album": "Remove from album",
|
"remove_from_album": "Remove from album",
|
||||||
"remove_from_album_action_prompt": "{count} removed from the album",
|
"remove_from_album_action_prompt": "{count} removed from the album",
|
||||||
"remove_from_favorites": "Remove from favorites",
|
"remove_from_favorites": "Remove from favorites",
|
||||||
@@ -2184,7 +2197,9 @@
|
|||||||
"show_in_timeline": "Show in timeline",
|
"show_in_timeline": "Show in timeline",
|
||||||
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
||||||
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
||||||
|
"show_less": "Show less",
|
||||||
"show_metadata": "Show metadata",
|
"show_metadata": "Show metadata",
|
||||||
|
"show_more_fields": "{count, plural, one {Show # more field} other {Show # more fields}}",
|
||||||
"show_or_hide_info": "Show or hide info",
|
"show_or_hide_info": "Show or hide info",
|
||||||
"show_password": "Show password",
|
"show_password": "Show password",
|
||||||
"show_person_options": "Show person options",
|
"show_person_options": "Show person options",
|
||||||
@@ -2236,6 +2251,11 @@
|
|||||||
"start_date_before_end_date": "Start date must be before end date",
|
"start_date_before_end_date": "Start date must be before end date",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
"step_delete": "Delete step",
|
||||||
|
"step_delete_confirm": "Are you sure you want to delete this step?",
|
||||||
|
"step_details": "Step details",
|
||||||
|
"steps": "Steps",
|
||||||
|
"steps_count": "{count, plural, one {# step} other {# steps}}",
|
||||||
"stop_casting": "Stop casting",
|
"stop_casting": "Stop casting",
|
||||||
"stop_motion_photo": "Stop Motion Photo",
|
"stop_motion_photo": "Stop Motion Photo",
|
||||||
"stop_photo_sharing": "Stop sharing your photos?",
|
"stop_photo_sharing": "Stop sharing your photos?",
|
||||||
@@ -2329,7 +2349,7 @@
|
|||||||
"trash_page_title": "Trash ({count})",
|
"trash_page_title": "Trash ({count})",
|
||||||
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"trigger_asset_uploaded": "Asset Uploaded",
|
"trigger_asset_uploaded": "Asset Upload",
|
||||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||||
"trigger_description": "An event that kicks off the workflow",
|
"trigger_description": "An event that kicks off the workflow",
|
||||||
"trigger_person_recognized": "Person Recognized",
|
"trigger_person_recognized": "Person Recognized",
|
||||||
@@ -2369,7 +2389,6 @@
|
|||||||
"unsupported_field_type": "Unsupported field type",
|
"unsupported_field_type": "Unsupported field type",
|
||||||
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
"untitled_workflow": "Untitled workflow",
|
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
||||||
"updated_at": "Updated",
|
"updated_at": "Updated",
|
||||||
@@ -2398,6 +2417,7 @@
|
|||||||
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
|
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
|
||||||
"use_current_connection": "Use current connection",
|
"use_current_connection": "Use current connection",
|
||||||
"use_custom_date_range": "Use custom date range instead",
|
"use_custom_date_range": "Use custom date range instead",
|
||||||
|
"use_template": "Use template",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"user_has_been_deleted": "This user has been deleted.",
|
"user_has_been_deleted": "This user has been deleted.",
|
||||||
"user_id": "User ID",
|
"user_id": "User ID",
|
||||||
@@ -2459,8 +2479,10 @@
|
|||||||
"week": "Week",
|
"week": "Week",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
|
"when": "When",
|
||||||
"width": "Width",
|
"width": "Width",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
|
"workflow": "Workflow",
|
||||||
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
|
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
|
||||||
"workflow_deleted": "Workflow deleted",
|
"workflow_deleted": "Workflow deleted",
|
||||||
"workflow_description": "Workflow description",
|
"workflow_description": "Workflow description",
|
||||||
@@ -2470,6 +2492,7 @@
|
|||||||
"workflow_name": "Workflow name",
|
"workflow_name": "Workflow name",
|
||||||
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
||||||
"workflow_summary": "Workflow summary",
|
"workflow_summary": "Workflow summary",
|
||||||
|
"workflow_templates": "Workflow templates",
|
||||||
"workflow_update_success": "Workflow updated successfully",
|
"workflow_update_success": "Workflow updated successfully",
|
||||||
"workflow_updated": "Workflow updated",
|
"workflow_updated": "Workflow updated",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "immich-i18n",
|
|
||||||
"version": "2.7.5",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"format": "prettier --cache --check .",
|
|
||||||
"format:fix": "prettier --cache --write --list-different ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^3.7.4",
|
|
||||||
"prettier-plugin-sort-json": "^4.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -64,16 +64,13 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
|||||||
|
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version
|
pnpm version "$NEXT_SERVER" --no-git-tag-version
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix i18n
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix cli
|
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
|
||||||
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
|
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
|
||||||
|
|
||||||
# copy version to open-api spec
|
# copy version to open-api spec
|
||||||
pnpm install --frozen-lockfile --prefix server
|
mise run //:open-api
|
||||||
pnpm --prefix server run build
|
|
||||||
( cd ./open-api && bash ./bin/generate-open-api.sh )
|
|
||||||
|
|
||||||
uv version --directory machine-learning "$NEXT_SERVER"
|
uv version --directory machine-learning "$NEXT_SERVER"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ experimental_monorepo_root = true
|
|||||||
|
|
||||||
[monorepo]
|
[monorepo]
|
||||||
config_roots = [
|
config_roots = [
|
||||||
"plugins",
|
"packages/plugin-core",
|
||||||
"server",
|
"server",
|
||||||
"cli",
|
"packages/cli",
|
||||||
"deployment",
|
"deployment",
|
||||||
"mobile",
|
"mobile",
|
||||||
"e2e",
|
"e2e",
|
||||||
@@ -21,12 +21,23 @@ pnpm = "10.33.1"
|
|||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
|
"npm:oazapfts" = "7.5.0"
|
||||||
|
"github:extism/cli" = "1.6.3"
|
||||||
|
"github:webassembly/binaryen" = "version_124"
|
||||||
|
"github:extism/js-pdk" = "1.6.0"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
bin = "dcm"
|
bin = "dcm"
|
||||||
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm".platforms]
|
||||||
|
linux-x64 = { asset_pattern = "dcm-linux-x64-release.zip" }
|
||||||
|
linux-arm64 = { asset_pattern = "dcm-linux-arm-release.zip" }
|
||||||
|
macos-x64 = { asset_pattern = "dcm-macos-x64-release.zip" }
|
||||||
|
macos-arm64 = { asset_pattern = "dcm-macos-arm-release.zip" }
|
||||||
|
windows-x64 = { asset_pattern = "dcm-windows-release.zip" }
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
|
|
||||||
@@ -40,20 +51,51 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
|
|||||||
experimental = true
|
experimental = true
|
||||||
pin = true
|
pin = true
|
||||||
|
|
||||||
|
[tasks.plugins]
|
||||||
|
run = [
|
||||||
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||||
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.open-api-typescript]
|
||||||
|
run = [
|
||||||
|
"oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
|
||||||
|
{ task = "//:sdk:install" },
|
||||||
|
{ task = "//:sdk:build" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.open-api-dart]
|
||||||
|
dir = "open-api"
|
||||||
|
run = "bash ./bin/generate-dart-sdk.sh"
|
||||||
|
|
||||||
|
[tasks.open-api]
|
||||||
|
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
||||||
|
run = [
|
||||||
|
{ task = "//:plugins" },
|
||||||
|
{ task = "//server:build" },
|
||||||
|
{ task = "//server:install" },
|
||||||
|
{ task = "//server:build" },
|
||||||
|
{ task = "//server:sync-open-api" },
|
||||||
|
{ task = ":open-api-typescript"},
|
||||||
|
{ task = ":open-api-dart"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.sql]
|
||||||
|
dir = "server"
|
||||||
|
run = "node ./dist/bin/sync-sql.js"
|
||||||
|
|
||||||
# SDK tasks
|
# SDK tasks
|
||||||
[tasks."sdk:install"]
|
[tasks."sdk:install"]
|
||||||
dir = "packages/sdk"
|
dir = "packages/sdk"
|
||||||
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
|
run = "pnpm --filter @immich/sdk install --frozen-lockfile"
|
||||||
|
|
||||||
[tasks."sdk:build"]
|
[tasks."sdk:build"]
|
||||||
dir = "packages/sdk"
|
dir = "packages/sdk"
|
||||||
run = "pnpm run build"
|
run = "pnpm build"
|
||||||
|
|
||||||
# i18n tasks
|
# i18n tasks
|
||||||
[tasks."i18n:format"]
|
[tasks."i18n:format"]
|
||||||
dir = "i18n"
|
run = "pnpm format"
|
||||||
run = "pnpm run format"
|
|
||||||
|
|
||||||
[tasks."i18n:format-fix"]
|
[tasks."i18n:format-fix"]
|
||||||
dir = "i18n"
|
run = "pnpm format:fix"
|
||||||
run = "pnpm run format:fix"
|
|
||||||
|
|||||||
+2
-2
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun onAndroidUpload(callback: (Result<Unit>) -> Unit)
|
fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
|
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
channel.send(null) {
|
channel.send(listOf(maxMinutesArg)) {
|
||||||
if (it is List<*>) {
|
if (it is List<*>) {
|
||||||
if (it.size > 1) {
|
if (it.size > 1) {
|
||||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
|||||||
+1
-1
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
* This method acts as a bridge between the native Android background task system and Flutter.
|
* This method acts as a bridge between the native Android background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
override fun onInitialized() {
|
override fun onInitialized() {
|
||||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to a separate NotificationManager class
|
// TODO: Move this to a separate NotificationManager class
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import java.io.IOException
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
||||||
|
|
||||||
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||||
|
|
||||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
@@ -228,7 +230,6 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
private val onComplete: () -> Unit,
|
private val onComplete: () -> Unit,
|
||||||
) : UrlRequest.Callback() {
|
) : UrlRequest.Callback() {
|
||||||
private var buffer: NativeByteBuffer? = null
|
private var buffer: NativeByteBuffer? = null
|
||||||
private var wrapped: ByteBuffer? = null
|
|
||||||
private var error: Exception? = null
|
private var error: Exception? = null
|
||||||
|
|
||||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
||||||
@@ -242,15 +243,16 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Content-Length is a size hint only. With Content-Encoding (gzip/br/...),
|
||||||
|
// Cronet auto-decompresses and writes decompressed bytes to our buffer, which
|
||||||
|
// may exceed the wire/compressed Content-Length. Always use the growable
|
||||||
|
// buffer path so we can't overflow.
|
||||||
val contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0
|
val contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0
|
||||||
if (contentLength > 0) {
|
// Cap the up-front alloc: Content-Length is untrusted and can be huge or near
|
||||||
buffer = NativeByteBuffer(contentLength + 1)
|
// Int.MAX_VALUE (overflowing `+1`). For larger responses the grow path takes over.
|
||||||
wrapped = NativeBuffer.wrap(buffer!!.pointer, contentLength + 1)
|
val initialSize = if (contentLength in 1..MAX_PREALLOC_BYTES) contentLength + 1 else INITIAL_BUFFER_SIZE
|
||||||
request.read(wrapped)
|
buffer = NativeByteBuffer(initialSize)
|
||||||
} else {
|
request.read(buffer!!.wrapRemaining())
|
||||||
buffer = NativeByteBuffer(INITIAL_BUFFER_SIZE)
|
|
||||||
request.read(buffer!!.wrapRemaining())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e
|
error = e
|
||||||
return request.cancel()
|
return request.cancel()
|
||||||
@@ -263,14 +265,14 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
byteBuffer: ByteBuffer
|
byteBuffer: ByteBuffer
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val buf = if (wrapped == null) {
|
// Always pass a fresh wrap so byteBuffer.position() represents only the
|
||||||
buffer!!.run {
|
// bytes Cronet wrote in this iteration. Reusing the caller-supplied
|
||||||
advance(byteBuffer.position())
|
// ByteBuffer breaks advance(): Cronet's position keeps accumulating
|
||||||
ensureHeadroom()
|
// across reads, which would double-count previous iterations' bytes.
|
||||||
wrapRemaining()
|
val buf = buffer!!.run {
|
||||||
}
|
advance(byteBuffer.position())
|
||||||
} else {
|
ensureHeadroom()
|
||||||
wrapped
|
wrapRemaining()
|
||||||
}
|
}
|
||||||
request.read(buf)
|
request.read(buf)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -280,7 +282,6 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||||
wrapped?.let { buffer!!.advance(it.position()) }
|
|
||||||
onSuccess(buffer!!)
|
onSuccess(buffer!!)
|
||||||
onComplete()
|
onComplete()
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-117
@@ -1003,20 +1003,6 @@
|
|||||||
1
|
1
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
|
||||||
"on": 1,
|
|
||||||
"name": "idx_remote_asset_owner_checksum",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
|
|
||||||
"unique": false,
|
|
||||||
"columns": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"references": [
|
|
||||||
1
|
|
||||||
],
|
|
||||||
"type": "index",
|
|
||||||
"data": {
|
"data": {
|
||||||
"on": 1,
|
"on": 1,
|
||||||
"name": "UQ_remote_assets_owner_checksum",
|
"name": "UQ_remote_assets_owner_checksum",
|
||||||
@@ -1026,7 +1012,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 12,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -1040,7 +1026,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 13,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -1054,7 +1040,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 14,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -1067,36 +1053,22 @@
|
|||||||
"columns": []
|
"columns": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"references": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"type": "index",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"name": "idx_remote_asset_owner_visibility_deleted_created",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
|
||||||
|
"unique": false,
|
||||||
|
"columns": []
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"references": [
|
|
||||||
1
|
|
||||||
],
|
|
||||||
"type": "index",
|
|
||||||
"data": {
|
|
||||||
"on": 1,
|
|
||||||
"name": "idx_remote_asset_local_date_time_day",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
|
|
||||||
"unique": false,
|
|
||||||
"columns": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 17,
|
|
||||||
"references": [
|
|
||||||
1
|
|
||||||
],
|
|
||||||
"type": "index",
|
|
||||||
"data": {
|
|
||||||
"on": 1,
|
|
||||||
"name": "idx_remote_asset_local_date_time_month",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
|
|
||||||
"unique": false,
|
|
||||||
"columns": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 18,
|
|
||||||
"references": [],
|
"references": [],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -1226,7 +1198,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 19,
|
"id": 17,
|
||||||
"references": [
|
"references": [
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
@@ -1301,7 +1273,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 20,
|
"id": 18,
|
||||||
"references": [
|
"references": [
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
@@ -1388,7 +1360,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 21,
|
"id": 19,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -1644,7 +1616,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 22,
|
"id": 20,
|
||||||
"references": [
|
"references": [
|
||||||
1,
|
1,
|
||||||
4
|
4
|
||||||
@@ -1718,7 +1690,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 23,
|
"id": 21,
|
||||||
"references": [
|
"references": [
|
||||||
4,
|
4,
|
||||||
0
|
0
|
||||||
@@ -1806,7 +1778,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 24,
|
"id": 22,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -1902,7 +1874,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 25,
|
"id": 23,
|
||||||
"references": [
|
"references": [
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
@@ -2066,10 +2038,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 26,
|
"id": 24,
|
||||||
"references": [
|
"references": [
|
||||||
1,
|
1,
|
||||||
25
|
23
|
||||||
],
|
],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -2140,7 +2112,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 27,
|
"id": 25,
|
||||||
"references": [
|
"references": [
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
@@ -2284,10 +2256,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 28,
|
"id": 26,
|
||||||
"references": [
|
"references": [
|
||||||
1,
|
1,
|
||||||
27
|
25
|
||||||
],
|
],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -2461,7 +2433,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 29,
|
"id": 27,
|
||||||
"references": [],
|
"references": [],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -2509,7 +2481,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 30,
|
"id": 28,
|
||||||
"references": [],
|
"references": [],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -2684,7 +2656,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 31,
|
"id": 29,
|
||||||
"references": [
|
"references": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
@@ -2778,7 +2750,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 32,
|
"id": 30,
|
||||||
"references": [],
|
"references": [],
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -2826,13 +2798,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 33,
|
"id": 31,
|
||||||
"references": [
|
"references": [
|
||||||
20
|
18
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 20,
|
"on": 18,
|
||||||
"name": "idx_partner_shared_with_id",
|
"name": "idx_partner_shared_with_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2840,19 +2812,47 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 34,
|
"id": 32,
|
||||||
"references": [
|
"references": [
|
||||||
21
|
19
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 21,
|
"on": 19,
|
||||||
"name": "idx_lat_lng",
|
"name": "idx_lat_lng",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
"columns": []
|
"columns": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"references": [
|
||||||
|
19
|
||||||
|
],
|
||||||
|
"type": "index",
|
||||||
|
"data": {
|
||||||
|
"on": 19,
|
||||||
|
"name": "idx_remote_exif_city",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
|
||||||
|
"unique": false,
|
||||||
|
"columns": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"references": [
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"type": "index",
|
||||||
|
"data": {
|
||||||
|
"on": 20,
|
||||||
|
"name": "idx_remote_album_asset_album_asset",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
|
||||||
|
"unique": false,
|
||||||
|
"columns": []
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 35,
|
"id": 35,
|
||||||
"references": [
|
"references": [
|
||||||
@@ -2861,20 +2861,6 @@
|
|||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 22,
|
"on": 22,
|
||||||
"name": "idx_remote_album_asset_album_asset",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
|
|
||||||
"unique": false,
|
|
||||||
"columns": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 36,
|
|
||||||
"references": [
|
|
||||||
24
|
|
||||||
],
|
|
||||||
"type": "index",
|
|
||||||
"data": {
|
|
||||||
"on": 24,
|
|
||||||
"name": "idx_remote_asset_cloud_id",
|
"name": "idx_remote_asset_cloud_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2882,13 +2868,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 37,
|
"id": 36,
|
||||||
"references": [
|
"references": [
|
||||||
27
|
25
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 27,
|
"on": 25,
|
||||||
"name": "idx_person_owner_id",
|
"name": "idx_person_owner_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2896,13 +2882,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 38,
|
"id": 37,
|
||||||
"references": [
|
"references": [
|
||||||
28
|
26
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 28,
|
"on": 26,
|
||||||
"name": "idx_asset_face_person_id",
|
"name": "idx_asset_face_person_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2910,13 +2896,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 39,
|
"id": 38,
|
||||||
"references": [
|
"references": [
|
||||||
28
|
26
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 28,
|
"on": 26,
|
||||||
"name": "idx_asset_face_asset_id",
|
"name": "idx_asset_face_asset_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2924,13 +2910,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 40,
|
"id": 39,
|
||||||
"references": [
|
"references": [
|
||||||
30
|
26
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 30,
|
"on": 26,
|
||||||
|
"name": "idx_asset_face_visible_person",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
|
||||||
|
"unique": false,
|
||||||
|
"columns": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"references": [
|
||||||
|
28
|
||||||
|
],
|
||||||
|
"type": "index",
|
||||||
|
"data": {
|
||||||
|
"on": 28,
|
||||||
"name": "idx_trashed_local_asset_checksum",
|
"name": "idx_trashed_local_asset_checksum",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2940,11 +2940,11 @@
|
|||||||
{
|
{
|
||||||
"id": 41,
|
"id": 41,
|
||||||
"references": [
|
"references": [
|
||||||
30
|
28
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 30,
|
"on": 28,
|
||||||
"name": "idx_trashed_local_asset_album",
|
"name": "idx_trashed_local_asset_album",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -2954,11 +2954,11 @@
|
|||||||
{
|
{
|
||||||
"id": 42,
|
"id": 42,
|
||||||
"references": [
|
"references": [
|
||||||
31
|
29
|
||||||
],
|
],
|
||||||
"type": "index",
|
"type": "index",
|
||||||
"data": {
|
"data": {
|
||||||
"on": 31,
|
"on": 29,
|
||||||
"name": "idx_asset_edit_asset_id",
|
"name": "idx_asset_edit_asset_id",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
|
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
@@ -3066,15 +3066,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "idx_remote_asset_owner_checksum",
|
|
||||||
"sql": [
|
|
||||||
{
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "UQ_remote_assets_owner_checksum",
|
"name": "UQ_remote_assets_owner_checksum",
|
||||||
"sql": [
|
"sql": [
|
||||||
@@ -3112,20 +3103,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_remote_asset_local_date_time_day",
|
"name": "idx_remote_asset_owner_visibility_deleted_created",
|
||||||
"sql": [
|
"sql": [
|
||||||
{
|
{
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))"
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_remote_asset_local_date_time_month",
|
|
||||||
"sql": [
|
|
||||||
{
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3282,6 +3264,15 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_remote_exif_city",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_remote_album_asset_album_asset",
|
"name": "idx_remote_album_asset_album_asset",
|
||||||
"sql": [
|
"sql": [
|
||||||
@@ -3327,6 +3318,15 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_asset_face_visible_person",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_trashed_local_asset_checksum",
|
"name": "idx_trashed_local_asset_checksum",
|
||||||
"sql": [
|
"sql": [
|
||||||
|
|||||||
+3368
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
|
|||||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
protocol BackgroundWorkerFlutterApiProtocol {
|
protocol BackgroundWorkerFlutterApiProtocol {
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
}
|
}
|
||||||
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
||||||
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
channel.sendMessage(nil) { response in
|
channel.sendMessage([maxMinutesArg] as [Any?]) { response in
|
||||||
guard let listResponse = response as? [Any?] else {
|
guard let listResponse = response as? [Any?] else {
|
||||||
completion(.failure(createConnectionError(withChannelName: channelName)))
|
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
|
|
||||||
var domainAlbum = PlatformAlbum(
|
var domainAlbum = PlatformAlbum(
|
||||||
id: album.localIdentifier,
|
id: album.localIdentifier,
|
||||||
name: album.localizedTitle!,
|
name: album.localizedTitle ?? album.localIdentifier,
|
||||||
updatedAt: nil,
|
updatedAt: nil,
|
||||||
isCloud: isCloud,
|
isCloud: isCloud,
|
||||||
assetCount: Int64(assets.count)
|
assetCount: Int64(assets.count)
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ enum CleanupStep { selectDate, scan, delete }
|
|||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
enum AssetDateAggregation { start, end }
|
enum AssetDateAggregation { start, end }
|
||||||
|
|
||||||
|
enum SlideshowLook { contain, cover, blurredBackground }
|
||||||
|
|
||||||
|
enum SlideshowDirection { forward, backward, shuffle }
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
|
final DateTime? uploadedAt;
|
||||||
|
final DateTime? deletedAt;
|
||||||
|
|
||||||
const RemoteAsset({
|
const RemoteAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -20,6 +22,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
required super.type,
|
required super.type,
|
||||||
required super.createdAt,
|
required super.createdAt,
|
||||||
required super.updatedAt,
|
required super.updatedAt,
|
||||||
|
this.uploadedAt,
|
||||||
super.width,
|
super.width,
|
||||||
super.height,
|
super.height,
|
||||||
super.durationMs,
|
super.durationMs,
|
||||||
@@ -29,6 +32,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
required super.isEdited,
|
required super.isEdited,
|
||||||
|
this.deletedAt,
|
||||||
}) : localAssetId = localId;
|
}) : localAssetId = localId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,6 +50,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
|
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
|
||||||
|
|
||||||
|
bool get isTrashed => deletedAt != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
@@ -55,6 +61,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
type: $type,
|
type: $type,
|
||||||
createdAt: $createdAt,
|
createdAt: $createdAt,
|
||||||
updatedAt: $updatedAt,
|
updatedAt: $updatedAt,
|
||||||
|
uploadedAt: ${uploadedAt ?? "<NA>"},
|
||||||
width: ${width ?? "<NA>"},
|
width: ${width ?? "<NA>"},
|
||||||
height: ${height ?? "<NA>"},
|
height: ${height ?? "<NA>"},
|
||||||
durationMs: ${durationMs ?? "<NA>"},
|
durationMs: ${durationMs ?? "<NA>"},
|
||||||
@@ -82,7 +89,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
ownerId == other.ownerId &&
|
ownerId == other.ownerId &&
|
||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility &&
|
visibility == other.visibility &&
|
||||||
stackId == other.stackId;
|
stackId == other.stackId &&
|
||||||
|
uploadedAt == other.uploadedAt &&
|
||||||
|
deletedAt == other.deletedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -93,7 +102,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode ^
|
visibility.hashCode ^
|
||||||
stackId.hashCode;
|
stackId.hashCode ^
|
||||||
|
uploadedAt.hashCode ^
|
||||||
|
deletedAt.hashCode;
|
||||||
|
|
||||||
RemoteAsset copyWith({
|
RemoteAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -104,6 +115,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
AssetType? type,
|
AssetType? type,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
|
DateTime? uploadedAt,
|
||||||
int? width,
|
int? width,
|
||||||
int? height,
|
int? height,
|
||||||
int? durationMs,
|
int? durationMs,
|
||||||
@@ -113,6 +125,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
String? livePhotoVideoId,
|
String? livePhotoVideoId,
|
||||||
String? stackId,
|
String? stackId,
|
||||||
bool? isEdited,
|
bool? isEdited,
|
||||||
|
DateTime? deletedAt,
|
||||||
}) {
|
}) {
|
||||||
return RemoteAsset(
|
return RemoteAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -123,6 +136,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
uploadedAt: uploadedAt ?? this.uploadedAt,
|
||||||
width: width ?? this.width,
|
width: width ?? this.width,
|
||||||
height: height ?? this.height,
|
height: height ?? this.height,
|
||||||
durationMs: durationMs ?? this.durationMs,
|
durationMs: durationMs ?? this.durationMs,
|
||||||
@@ -132,6 +146,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
stackId: stackId ?? this.stackId,
|
stackId: stackId ?? this.stackId,
|
||||||
isEdited: isEdited ?? this.isEdited,
|
isEdited: isEdited ?? this.isEdited,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,6 +163,8 @@ class RemoteAssetExif extends RemoteAsset {
|
|||||||
required super.type,
|
required super.type,
|
||||||
required super.createdAt,
|
required super.createdAt,
|
||||||
required super.updatedAt,
|
required super.updatedAt,
|
||||||
|
super.uploadedAt,
|
||||||
|
super.deletedAt,
|
||||||
super.width,
|
super.width,
|
||||||
super.height,
|
super.height,
|
||||||
super.durationMs,
|
super.durationMs,
|
||||||
@@ -184,6 +201,8 @@ class RemoteAssetExif extends RemoteAsset {
|
|||||||
AssetType? type,
|
AssetType? type,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
|
DateTime? uploadedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
int? width,
|
int? width,
|
||||||
int? height,
|
int? height,
|
||||||
int? durationMs,
|
int? durationMs,
|
||||||
@@ -204,6 +223,8 @@ class RemoteAssetExif extends RemoteAsset {
|
|||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
uploadedAt: uploadedAt ?? this.uploadedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
width: width ?? this.width,
|
width: width ?? this.width,
|
||||||
height: height ?? this.height,
|
height: height ?? this.height,
|
||||||
durationMs: durationMs ?? this.durationMs,
|
durationMs: durationMs ?? this.durationMs,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
|
class AlbumConfig {
|
||||||
|
final AlbumSortMode sortMode;
|
||||||
|
final bool isReverse;
|
||||||
|
final bool isGrid;
|
||||||
|
|
||||||
|
const AlbumConfig({this.sortMode = AlbumSortMode.mostRecent, this.isReverse = true, this.isGrid = false});
|
||||||
|
|
||||||
|
AlbumConfig copyWith({AlbumSortMode? sortMode, bool? isReverse, bool? isGrid}) => AlbumConfig(
|
||||||
|
sortMode: sortMode ?? this.sortMode,
|
||||||
|
isReverse: isReverse ?? this.isReverse,
|
||||||
|
isGrid: isGrid ?? this.isGrid,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is AlbumConfig && other.sortMode == sortMode && other.isReverse == isReverse && other.isGrid == isGrid);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(sortMode, isReverse, isGrid);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AlbumConfig(sortMode: $sortMode, isReverse: $isReverse, isGrid: $isGrid)';
|
||||||
|
}
|
||||||
@@ -1,22 +1,70 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/config/album_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/image_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/map_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
||||||
|
|
||||||
class AppConfig {
|
class AppConfig {
|
||||||
final ThemeConfig theme;
|
final ThemeConfig theme;
|
||||||
final CleanupConfig cleanup;
|
final CleanupConfig cleanup;
|
||||||
|
final MapConfig map;
|
||||||
|
final TimelineConfig timeline;
|
||||||
|
final ImageConfig image;
|
||||||
|
final ViewerConfig viewer;
|
||||||
|
final SlideshowConfig slideshow;
|
||||||
|
final AlbumConfig album;
|
||||||
|
|
||||||
const AppConfig({this.theme = const .new(), this.cleanup = const .new()});
|
const AppConfig({
|
||||||
|
this.theme = const .new(),
|
||||||
|
this.cleanup = const .new(),
|
||||||
|
this.map = const .new(),
|
||||||
|
this.timeline = const .new(),
|
||||||
|
this.image = const .new(),
|
||||||
|
this.viewer = const .new(),
|
||||||
|
this.slideshow = const .new(),
|
||||||
|
this.album = const .new(),
|
||||||
|
});
|
||||||
|
|
||||||
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup}) =>
|
AppConfig copyWith({
|
||||||
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup);
|
ThemeConfig? theme,
|
||||||
|
CleanupConfig? cleanup,
|
||||||
|
MapConfig? map,
|
||||||
|
TimelineConfig? timeline,
|
||||||
|
ImageConfig? image,
|
||||||
|
ViewerConfig? viewer,
|
||||||
|
SlideshowConfig? slideshow,
|
||||||
|
AlbumConfig? album,
|
||||||
|
}) => .new(
|
||||||
|
theme: theme ?? this.theme,
|
||||||
|
cleanup: cleanup ?? this.cleanup,
|
||||||
|
map: map ?? this.map,
|
||||||
|
timeline: timeline ?? this.timeline,
|
||||||
|
image: image ?? this.image,
|
||||||
|
viewer: viewer ?? this.viewer,
|
||||||
|
slideshow: slideshow ?? this.slideshow,
|
||||||
|
album: album ?? this.album,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) || (other is AppConfig && other.theme == theme && other.cleanup == cleanup);
|
identical(this, other) ||
|
||||||
|
(other is AppConfig &&
|
||||||
|
other.theme == theme &&
|
||||||
|
other.cleanup == cleanup &&
|
||||||
|
other.map == map &&
|
||||||
|
other.timeline == timeline &&
|
||||||
|
other.image == image &&
|
||||||
|
other.viewer == viewer &&
|
||||||
|
other.slideshow == slideshow &&
|
||||||
|
other.album == album);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(theme, cleanup);
|
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup)';
|
String toString() =>
|
||||||
|
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class ImageConfig {
|
||||||
|
final bool preferRemote;
|
||||||
|
final bool loadOriginal;
|
||||||
|
|
||||||
|
const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
|
||||||
|
|
||||||
|
ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
|
||||||
|
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(preferRemote, loadOriginal);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MapConfig {
|
||||||
|
final int relativeDays;
|
||||||
|
final bool favoritesOnly;
|
||||||
|
final bool includeArchived;
|
||||||
|
final ThemeMode themeMode;
|
||||||
|
final bool withPartners;
|
||||||
|
|
||||||
|
const MapConfig({
|
||||||
|
this.relativeDays = 0,
|
||||||
|
this.favoritesOnly = false,
|
||||||
|
this.includeArchived = false,
|
||||||
|
this.themeMode = ThemeMode.system,
|
||||||
|
this.withPartners = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
MapConfig copyWith({
|
||||||
|
int? relativeDays,
|
||||||
|
bool? favoritesOnly,
|
||||||
|
bool? includeArchived,
|
||||||
|
ThemeMode? themeMode,
|
||||||
|
bool? withPartners,
|
||||||
|
}) => MapConfig(
|
||||||
|
relativeDays: relativeDays ?? this.relativeDays,
|
||||||
|
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
|
||||||
|
includeArchived: includeArchived ?? this.includeArchived,
|
||||||
|
themeMode: themeMode ?? this.themeMode,
|
||||||
|
withPartners: withPartners ?? this.withPartners,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is MapConfig &&
|
||||||
|
other.relativeDays == relativeDays &&
|
||||||
|
other.favoritesOnly == favoritesOnly &&
|
||||||
|
other.includeArchived == includeArchived &&
|
||||||
|
other.themeMode == themeMode &&
|
||||||
|
other.withPartners == withPartners);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class NetworkConfig {
|
||||||
|
final bool autoEndpointSwitching;
|
||||||
|
final String? preferredWifiName;
|
||||||
|
final String? localEndpoint;
|
||||||
|
final List<String> externalEndpointList;
|
||||||
|
final Map<String, String> customHeaders;
|
||||||
|
|
||||||
|
const NetworkConfig({
|
||||||
|
this.autoEndpointSwitching = false,
|
||||||
|
this.preferredWifiName,
|
||||||
|
this.localEndpoint,
|
||||||
|
this.externalEndpointList = const [],
|
||||||
|
this.customHeaders = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
NetworkConfig copyWith({
|
||||||
|
bool? autoEndpointSwitching,
|
||||||
|
String? preferredWifiName,
|
||||||
|
String? localEndpoint,
|
||||||
|
List<String>? externalEndpointList,
|
||||||
|
Map<String, String>? customHeaders,
|
||||||
|
}) => NetworkConfig(
|
||||||
|
autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching,
|
||||||
|
preferredWifiName: preferredWifiName ?? this.preferredWifiName,
|
||||||
|
localEndpoint: localEndpoint ?? this.localEndpoint,
|
||||||
|
externalEndpointList: externalEndpointList ?? this.externalEndpointList,
|
||||||
|
customHeaders: customHeaders ?? this.customHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is NetworkConfig &&
|
||||||
|
other.autoEndpointSwitching == autoEndpointSwitching &&
|
||||||
|
other.preferredWifiName == preferredWifiName &&
|
||||||
|
other.localEndpoint == localEndpoint &&
|
||||||
|
listEquals(other.externalEndpointList, externalEndpointList) &&
|
||||||
|
mapEquals(other.customHeaders, customHeaders));
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
autoEndpointSwitching,
|
||||||
|
preferredWifiName,
|
||||||
|
localEndpoint,
|
||||||
|
Object.hashAll(externalEndpointList),
|
||||||
|
Object.hashAllUnordered(customHeaders.entries.map((e) => Object.hash(e.key, e.value))),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'NetworkConfig(autoEndpointSwitching: $autoEndpointSwitching, preferredWifiName: $preferredWifiName, localEndpoint: $localEndpoint, externalEndpointList: $externalEndpointList, customHeaders: $customHeaders)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
|
||||||
|
class SlideshowConfig {
|
||||||
|
final bool transition;
|
||||||
|
final bool repeat;
|
||||||
|
final int duration;
|
||||||
|
final SlideshowLook look;
|
||||||
|
final SlideshowDirection direction;
|
||||||
|
|
||||||
|
const SlideshowConfig({
|
||||||
|
this.transition = true,
|
||||||
|
this.repeat = true,
|
||||||
|
this.duration = 5,
|
||||||
|
this.look = SlideshowLook.contain,
|
||||||
|
this.direction = SlideshowDirection.forward,
|
||||||
|
});
|
||||||
|
|
||||||
|
SlideshowConfig copyWith({
|
||||||
|
bool? transition,
|
||||||
|
bool? repeat,
|
||||||
|
int? duration,
|
||||||
|
SlideshowLook? look,
|
||||||
|
SlideshowDirection? direction,
|
||||||
|
}) => SlideshowConfig(
|
||||||
|
transition: transition ?? this.transition,
|
||||||
|
repeat: repeat ?? this.repeat,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
look: look ?? this.look,
|
||||||
|
direction: direction ?? this.direction,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is SlideshowConfig &&
|
||||||
|
other.transition == transition &&
|
||||||
|
other.repeat == repeat &&
|
||||||
|
other.duration == duration &&
|
||||||
|
other.look == look &&
|
||||||
|
other.direction == direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(transition, repeat, duration, look, direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SlideshowConfig(transition: $transition, repeat: $repeat, duration: $duration, look: $look, direction: $direction)';
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/config/network_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
|
||||||
class SystemConfig {
|
class SystemConfig {
|
||||||
final LogLevel logLevel;
|
final LogLevel logLevel;
|
||||||
|
final NetworkConfig network;
|
||||||
|
|
||||||
const SystemConfig({this.logLevel = .info});
|
const SystemConfig({this.logLevel = .info, this.network = const .new()});
|
||||||
|
|
||||||
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
|
SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) =>
|
||||||
|
SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => logLevel.hashCode;
|
int get hashCode => Object.hash(logLevel, network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfig(logLevel: $logLevel)';
|
String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
|
||||||
|
class TimelineConfig {
|
||||||
|
final int tilesPerRow;
|
||||||
|
final GroupAssetsBy groupAssetsBy;
|
||||||
|
final bool storageIndicator;
|
||||||
|
|
||||||
|
const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true});
|
||||||
|
|
||||||
|
TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig(
|
||||||
|
tilesPerRow: tilesPerRow ?? this.tilesPerRow,
|
||||||
|
groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy,
|
||||||
|
storageIndicator: storageIndicator ?? this.storageIndicator,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is TimelineConfig &&
|
||||||
|
other.tilesPerRow == tilesPerRow &&
|
||||||
|
other.groupAssetsBy == groupAssetsBy &&
|
||||||
|
other.storageIndicator == storageIndicator);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
class ViewerConfig {
|
||||||
|
final bool loopVideo;
|
||||||
|
final bool loadOriginalVideo;
|
||||||
|
final bool autoPlayVideo;
|
||||||
|
final bool tapToNavigate;
|
||||||
|
|
||||||
|
const ViewerConfig({
|
||||||
|
this.loopVideo = true,
|
||||||
|
this.loadOriginalVideo = false,
|
||||||
|
this.autoPlayVideo = true,
|
||||||
|
this.tapToNavigate = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) =>
|
||||||
|
ViewerConfig(
|
||||||
|
loopVideo: loopVideo ?? this.loopVideo,
|
||||||
|
loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo,
|
||||||
|
autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo,
|
||||||
|
tapToNavigate: tapToNavigate ?? this.tapToNavigate,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ViewerConfig &&
|
||||||
|
other.loopVideo == loopVideo &&
|
||||||
|
other.loadOriginalVideo == loadOriginalVideo &&
|
||||||
|
other.autoPlayVideo == autoPlayVideo &&
|
||||||
|
other.tapToNavigate == tapToNavigate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)';
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
enum MetadataDomain<T extends Object> {
|
enum MetadataDomain<T extends Object> {
|
||||||
appConfig<AppConfig>('config.app'),
|
appConfig<AppConfig>('config.app'),
|
||||||
@@ -23,9 +25,63 @@ enum MetadataKey<T extends Object> {
|
|||||||
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
||||||
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
|
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
|
||||||
|
|
||||||
|
// Image
|
||||||
|
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
|
||||||
|
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
|
||||||
|
|
||||||
|
// Viewer
|
||||||
|
viewerLoopVideo<bool>(.appConfig, 'viewer.loopVideo', true),
|
||||||
|
viewerLoadOriginalVideo<bool>(.appConfig, 'viewer.loadOriginalVideo', false),
|
||||||
|
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
|
||||||
|
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
|
||||||
|
|
||||||
|
// Network
|
||||||
|
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
|
||||||
|
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
|
||||||
|
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
|
||||||
|
networkExternalEndpointList<List<String>>(
|
||||||
|
.systemConfig,
|
||||||
|
'network.externalEndpointList',
|
||||||
|
[],
|
||||||
|
_ListCodec(_PrimitiveCodec.string),
|
||||||
|
),
|
||||||
|
networkCustomHeaders<Map<String, String>>(
|
||||||
|
.systemConfig,
|
||||||
|
'network.customHeaders',
|
||||||
|
{},
|
||||||
|
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Album
|
||||||
|
albumSortMode<AlbumSortMode>(
|
||||||
|
.appConfig,
|
||||||
|
'album.sortMode',
|
||||||
|
AlbumSortMode.mostRecent,
|
||||||
|
_EnumCodec(AlbumSortMode.values),
|
||||||
|
),
|
||||||
|
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
|
||||||
|
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
|
||||||
|
|
||||||
|
// Timeline
|
||||||
|
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
||||||
|
timelineGroupAssetsBy<GroupAssetsBy>(
|
||||||
|
.appConfig,
|
||||||
|
'timeline.groupAssetsBy',
|
||||||
|
GroupAssetsBy.day,
|
||||||
|
_EnumCodec(GroupAssetsBy.values),
|
||||||
|
),
|
||||||
|
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
|
// Map
|
||||||
|
mapShowFavoriteOnly<bool>(.appConfig, 'map.showFavoriteOnly', false),
|
||||||
|
mapRelativeDate<int>(.appConfig, 'map.relativeDate', 0),
|
||||||
|
mapIncludeArchived<bool>(.appConfig, 'map.includeArchived', false),
|
||||||
|
mapThemeMode<ThemeMode>(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
|
||||||
|
mapWithPartners<bool>(.appConfig, 'map.withPartners', false),
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
|
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
|
||||||
cleanupKeepMediaType<AssetKeepType>(
|
cleanupKeepMediaType<AssetKeepType>(
|
||||||
@@ -36,7 +92,19 @@ enum MetadataKey<T extends Object> {
|
|||||||
),
|
),
|
||||||
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
||||||
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
||||||
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false);
|
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false),
|
||||||
|
|
||||||
|
// Slideshow
|
||||||
|
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
|
||||||
|
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
|
||||||
|
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
|
||||||
|
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
|
||||||
|
slideshowDirection<SlideshowDirection>(
|
||||||
|
.appConfig,
|
||||||
|
'slideshow.direction',
|
||||||
|
SlideshowDirection.forward,
|
||||||
|
_EnumCodec(SlideshowDirection.values),
|
||||||
|
);
|
||||||
|
|
||||||
final MetadataDomain domain;
|
final MetadataDomain domain;
|
||||||
final String name;
|
final String name;
|
||||||
@@ -103,6 +171,47 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
|||||||
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
||||||
|
final _MetadataCodec<K> _keyCodec;
|
||||||
|
final _MetadataCodec<V> _valueCodec;
|
||||||
|
|
||||||
|
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(Map<K, V> value) {
|
||||||
|
final entries = <String, String>{};
|
||||||
|
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||||
|
return jsonEncode(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<K, V>? decode(String raw) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final result = <K, V>{};
|
||||||
|
for (final entry in decoded.entries) {
|
||||||
|
final rawKey = entry.key;
|
||||||
|
final rawValue = entry.value;
|
||||||
|
if (rawKey is! String || rawValue is! String) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final k = _keyCodec.decode(rawKey);
|
||||||
|
final v = _valueCodec.decode(rawValue);
|
||||||
|
if (k == null || v == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
result[k] = v;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} on FormatException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
||||||
final _MetadataCodec<T> _elementCodec;
|
final _MetadataCodec<T> _elementCodec;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
|
||||||
enum Setting<T> {
|
enum Setting<T> {
|
||||||
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
|
||||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
|
||||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
|
||||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
|
||||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
|
||||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
|
||||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
|
||||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||||
enableBackup<bool>(StoreKey.enableBackup, false);
|
enableBackup<bool>(StoreKey.enableBackup, false);
|
||||||
|
|
||||||
|
|||||||
@@ -4,86 +4,42 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
/// Defines the data type for each value
|
/// Defines the data type for each value
|
||||||
enum StoreKey<T> {
|
enum StoreKey<T> {
|
||||||
version<int>._(0),
|
version<int>._(0),
|
||||||
assetETag<String>._(1),
|
|
||||||
currentUser<UserDto>._(2),
|
currentUser<UserDto>._(2),
|
||||||
deviceIdHash<int>._(3),
|
|
||||||
deviceId<String>._(4),
|
deviceId<String>._(4),
|
||||||
backupFailedSince<DateTime>._(5),
|
|
||||||
backupRequireWifi<bool>._(6),
|
|
||||||
backupRequireCharging<bool>._(7),
|
backupRequireCharging<bool>._(7),
|
||||||
backupTriggerDelay<int>._(8),
|
backupTriggerDelay<int>._(8),
|
||||||
serverUrl<String>._(10),
|
serverUrl<String>._(10),
|
||||||
accessToken<String>._(11),
|
accessToken<String>._(11),
|
||||||
serverEndpoint<String>._(12),
|
serverEndpoint<String>._(12),
|
||||||
autoBackup<bool>._(13),
|
|
||||||
backgroundBackup<bool>._(14),
|
|
||||||
sslClientCertData<String>._(15),
|
|
||||||
sslClientPasswd<String>._(16),
|
|
||||||
// user settings from [AppSettingsEnum] below:
|
|
||||||
loadPreview<bool>._(100),
|
|
||||||
loadOriginal<bool>._(101),
|
|
||||||
tilesPerRow<int>._(103),
|
|
||||||
dynamicLayout<bool>._(104),
|
|
||||||
groupAssetsBy<int>._(105),
|
|
||||||
uploadErrorNotificationGracePeriod<int>._(106),
|
|
||||||
backgroundBackupTotalProgress<bool>._(107),
|
|
||||||
backgroundBackupSingleProgress<bool>._(108),
|
|
||||||
storageIndicator<bool>._(109),
|
|
||||||
thumbnailCacheSize<int>._(110),
|
|
||||||
imageCacheSize<int>._(111),
|
|
||||||
albumThumbnailCacheSize<int>._(112),
|
|
||||||
selectedAlbumSortOrder<int>._(113),
|
|
||||||
advancedTroubleshooting<bool>._(114),
|
advancedTroubleshooting<bool>._(114),
|
||||||
preferRemoteImage<bool>._(116),
|
|
||||||
loopVideo<bool>._(117),
|
|
||||||
// map related settings
|
|
||||||
mapShowFavoriteOnly<bool>._(118),
|
|
||||||
mapRelativeDate<int>._(119),
|
|
||||||
selfSignedCert<bool>._(120),
|
|
||||||
mapIncludeArchived<bool>._(121),
|
|
||||||
ignoreIcloudAssets<bool>._(122),
|
|
||||||
selectedAlbumSortReverse<bool>._(123),
|
|
||||||
mapThemeMode<int>._(124),
|
|
||||||
mapwithPartners<bool>._(125),
|
|
||||||
enableHapticFeedback<bool>._(126),
|
enableHapticFeedback<bool>._(126),
|
||||||
customHeaders<String>._(127),
|
|
||||||
|
|
||||||
syncAlbums<bool>._(131),
|
syncAlbums<bool>._(131),
|
||||||
|
|
||||||
// Auto endpoint switching
|
|
||||||
autoEndpointSwitching<bool>._(132),
|
|
||||||
preferredWifiName<String>._(133),
|
|
||||||
localEndpoint<String>._(134),
|
|
||||||
externalEndpointList<String>._(135),
|
|
||||||
|
|
||||||
// Video settings
|
|
||||||
loadOriginalVideo<bool>._(136),
|
|
||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
// Read-only Mode settings
|
// Read-only Mode settings
|
||||||
readonlyModeEnabled<bool>._(138),
|
readonlyModeEnabled<bool>._(138),
|
||||||
|
|
||||||
autoPlayVideo<bool>._(139),
|
|
||||||
albumGridView<bool>._(140),
|
|
||||||
|
|
||||||
// Image viewer navigation settings
|
|
||||||
tapToNavigate<bool>._(141),
|
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
|
||||||
betaPromptShown<bool>._(1001),
|
|
||||||
betaTimeline<bool>._(1002),
|
|
||||||
enableBackup<bool>._(1003),
|
enableBackup<bool>._(1003),
|
||||||
useWifiForUploadVideos<bool>._(1004),
|
useWifiForUploadVideos<bool>._(1004),
|
||||||
useWifiForUploadPhotos<bool>._(1005),
|
useWifiForUploadPhotos<bool>._(1005),
|
||||||
needBetaMigration<bool>._(1006),
|
|
||||||
// TODO: Remove this after patching open-api
|
|
||||||
shouldResetSync<bool>._(1007),
|
|
||||||
|
|
||||||
// Free up space
|
|
||||||
syncMigrationStatus<String>._(1013),
|
syncMigrationStatus<String>._(1013),
|
||||||
|
|
||||||
// Legacy keys that have been migrated to the new metadata store
|
// Legacy keys that have been migrated to the new metadata store
|
||||||
|
legacySelectedAlbumSortOrder<int>._(113),
|
||||||
|
legacySelectedAlbumSortReverse<bool>._(123),
|
||||||
|
legacyAlbumGridView<bool>._(140),
|
||||||
|
legacyAutoEndpointSwitching<bool>._(132),
|
||||||
|
legacyPreferredWifiName<String>._(133),
|
||||||
|
legacyLocalEndpoint<String>._(134),
|
||||||
|
legacyExternalEndpointList<String>._(135),
|
||||||
|
legacyCustomHeaders<String>._(127),
|
||||||
|
legacyLoopVideo<bool>._(117),
|
||||||
|
legacyLoadOriginalVideo<bool>._(136),
|
||||||
|
legacyAutoPlayVideo<bool>._(139),
|
||||||
|
legacyTapToNavigate<bool>._(141),
|
||||||
|
legacyPreferRemoteImage<bool>._(116),
|
||||||
|
legacyLoadOriginal<bool>._(101),
|
||||||
legacyPrimaryColor<String>._(128),
|
legacyPrimaryColor<String>._(128),
|
||||||
legacyDynamicTheme<bool>._(129),
|
legacyDynamicTheme<bool>._(129),
|
||||||
legacyColorfulInterface<bool>._(130),
|
legacyColorfulInterface<bool>._(130),
|
||||||
@@ -93,6 +49,14 @@ enum StoreKey<T> {
|
|||||||
legacyCleanupKeepAlbumIds<String>._(1010),
|
legacyCleanupKeepAlbumIds<String>._(1010),
|
||||||
legacyCleanupCutoffDaysAgo<int>._(1011),
|
legacyCleanupCutoffDaysAgo<int>._(1011),
|
||||||
legacyCleanupDefaultsInitialized<bool>._(1012),
|
legacyCleanupDefaultsInitialized<bool>._(1012),
|
||||||
|
legacyTilesPerRow<int>._(103),
|
||||||
|
legacyGroupAssetsBy<int>._(105),
|
||||||
|
legacyStorageIndicator<bool>._(109),
|
||||||
|
legacyMapRelativeDate<int>._(119),
|
||||||
|
legacyMapShowFavoriteOnly<bool>._(118),
|
||||||
|
legacyMapIncludeArchived<bool>._(121),
|
||||||
|
legacyMapThemeMode<int>._(124),
|
||||||
|
legacyMapwithPartners<bool>._(125),
|
||||||
legacyLogLevel<int>._(115);
|
legacyLogLevel<int>._(115);
|
||||||
|
|
||||||
const StoreKey._(this.id);
|
const StoreKey._(this.id);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ enum GroupAssetsBy { day, month, auto, none }
|
|||||||
|
|
||||||
enum HeaderType { none, month, day, monthAndDay }
|
enum HeaderType { none, month, day, monthAndDay }
|
||||||
|
|
||||||
|
enum SortAssetsBy { taken, uploaded }
|
||||||
|
|
||||||
class Bucket {
|
class Bucket {
|
||||||
final int assetCount;
|
final int assetCount;
|
||||||
|
|
||||||
|
|||||||
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload(int? maxMinutes) async {
|
||||||
_logger.info('Android background processing started');
|
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
final sw = Stopwatch()..start();
|
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
|
||||||
try {
|
return _backgroundLoop(
|
||||||
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
|
hashTimeout: hashTimeout,
|
||||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
backupTimeout: backupTimeout,
|
||||||
return;
|
debugLabel: 'Android background upload',
|
||||||
}
|
);
|
||||||
await _handleBackup();
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to complete Android background processing", error, stack);
|
|
||||||
} finally {
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
await _cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
|
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
|
||||||
|
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _backgroundLoop({
|
||||||
|
required Duration hashTimeout,
|
||||||
|
required Duration? backupTimeout,
|
||||||
|
required String debugLabel,
|
||||||
|
}) async {
|
||||||
|
_logger.info(
|
||||||
|
'$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
|
||||||
|
);
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
if (!await _syncAssets(hashTimeout: hashTimeout)) {
|
||||||
if (!await _syncAssets(hashTimeout: timeout)) {
|
|
||||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupFuture = _handleBackup();
|
final backupFuture = _handleBackup();
|
||||||
if (maxSeconds != null) {
|
Timer? cancelTimer;
|
||||||
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
if (backupTimeout != null) {
|
||||||
} else {
|
cancelTimer = Timer(backupTimeout, () {
|
||||||
|
if (!_cancellationToken.isCompleted) {
|
||||||
|
_logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
|
||||||
|
_cancellationToken.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
await backupFuture;
|
await backupFuture;
|
||||||
|
} finally {
|
||||||
|
cancelTimer?.cancel();
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
_logger.severe("Failed to complete $debugLabel", error, stack);
|
||||||
} finally {
|
} finally {
|
||||||
sw.stop();
|
sw.stop();
|
||||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
_logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
|
||||||
await _cleanup();
|
await _cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
|
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
|
||||||
|
|
||||||
_logger.info("Cleaning up background worker");
|
_logger.info("Cleaning up background worker");
|
||||||
_cancellationToken.complete();
|
if (!_cancellationToken.isCompleted) {
|
||||||
|
_cancellationToken.complete();
|
||||||
|
}
|
||||||
final cleanupFutures = [
|
final cleanupFutures = [
|
||||||
nativeSyncApi?.cancelHashing(),
|
nativeSyncApi?.cancelHashing(),
|
||||||
workerManagerPatch.dispose().catchError((_) async {
|
workerManagerPatch.dispose().catchError((_) async {
|
||||||
|
|||||||
@@ -93,8 +93,7 @@ class LocalSyncService {
|
|||||||
|
|
||||||
if (CurrentPlatform.isIOS) {
|
if (CurrentPlatform.isIOS) {
|
||||||
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
|
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
|
||||||
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
|
// does not include changes for cloud albums.
|
||||||
// remove the albums from the local database from the previous sync
|
|
||||||
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
|
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
|
||||||
for (final album in cloudAlbums) {
|
for (final album in cloudAlbums) {
|
||||||
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
|
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
|
||||||
|
|||||||
@@ -9,12 +9,47 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor
|
|||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// Categorizes a heterogeneous asset selection into the candidates that can
|
||||||
|
/// be added to an album immediately (already on the server) and the local-only
|
||||||
|
/// candidates that must be uploaded first.
|
||||||
|
class AlbumAssetCandidates {
|
||||||
|
final List<String> remoteAssetIds;
|
||||||
|
final List<LocalAsset> localAssetsToUpload;
|
||||||
|
|
||||||
|
const AlbumAssetCandidates({required this.remoteAssetIds, required this.localAssetsToUpload});
|
||||||
|
}
|
||||||
|
|
||||||
class RemoteAlbumService {
|
class RemoteAlbumService {
|
||||||
|
static final _logger = Logger('RemoteAlbumService');
|
||||||
|
|
||||||
final DriftRemoteAlbumRepository _repository;
|
final DriftRemoteAlbumRepository _repository;
|
||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
final ForegroundUploadService _uploadService;
|
||||||
|
|
||||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
const RemoteAlbumService(this._repository, this._albumApiRepository, this._uploadService);
|
||||||
|
|
||||||
|
/// Categorizes a heterogeneous asset selection into already-on-server IDs
|
||||||
|
/// and local assets that still need to be uploaded.
|
||||||
|
static AlbumAssetCandidates categorizeCandidates(Iterable<BaseAsset> assets) {
|
||||||
|
final remoteIds = <String>[];
|
||||||
|
final localToUpload = <LocalAsset>[];
|
||||||
|
for (final asset in assets) {
|
||||||
|
if (asset is RemoteAsset) {
|
||||||
|
remoteIds.add(asset.id);
|
||||||
|
} else if (asset is LocalAsset) {
|
||||||
|
final remoteId = asset.remoteId;
|
||||||
|
if (remoteId != null) {
|
||||||
|
remoteIds.add(remoteId);
|
||||||
|
} else {
|
||||||
|
localToUpload.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AlbumAssetCandidates(remoteAssetIds: remoteIds, localAssetsToUpload: localToUpload);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
return _repository.watchAlbum(albumId);
|
return _repository.watchAlbum(albumId);
|
||||||
@@ -148,6 +183,122 @@ class RemoteAlbumService {
|
|||||||
return album.added.length;
|
return album.added.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// !TODO The name here is not clear as we have addAssets method above,
|
||||||
|
/// which is only add remote assets to album, for the next PR, we will allow
|
||||||
|
/// adding local assets from album from the timeline as well with this flow.
|
||||||
|
/// So saving that for the next refactor
|
||||||
|
Future<int> addAssetsToAlbum({
|
||||||
|
required String albumId,
|
||||||
|
required UserDto uploader,
|
||||||
|
required AlbumAssetCandidates candidates,
|
||||||
|
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||||
|
}) async {
|
||||||
|
int addedCount = 0;
|
||||||
|
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||||
|
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
||||||
|
}
|
||||||
|
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||||
|
addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks);
|
||||||
|
}
|
||||||
|
return addedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an album, seeding it with already-remote asset IDs, then uploads
|
||||||
|
/// local-only assets and links each one as it finishes.
|
||||||
|
Future<RemoteAlbum> createAlbumWithAssets({
|
||||||
|
required String title,
|
||||||
|
required UserDto owner,
|
||||||
|
String? description,
|
||||||
|
AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []),
|
||||||
|
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||||
|
}) async {
|
||||||
|
final album = await createAlbum(
|
||||||
|
title: title,
|
||||||
|
owner: owner,
|
||||||
|
description: description,
|
||||||
|
assetIds: candidates.remoteAssetIds,
|
||||||
|
);
|
||||||
|
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||||
|
await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks);
|
||||||
|
}
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _uploadAndAddLocals(
|
||||||
|
String albumId,
|
||||||
|
UserDto uploader,
|
||||||
|
List<LocalAsset> localAssets,
|
||||||
|
UploadCallbacks userCallbacks,
|
||||||
|
) async {
|
||||||
|
int addedCount = 0;
|
||||||
|
final pendingAdds = <Future<void>>[];
|
||||||
|
final localById = {for (final a in localAssets) a.id: a};
|
||||||
|
|
||||||
|
final wrappedCallbacks = UploadCallbacks(
|
||||||
|
onProgress: (localId, filename, bytes, totalBytes) => _runUploadCallback(
|
||||||
|
'Upload progress callback failed for $localId',
|
||||||
|
() => userCallbacks.onProgress?.call(localId, filename, bytes, totalBytes),
|
||||||
|
),
|
||||||
|
onICloudProgress: (localId, progress) => _runUploadCallback(
|
||||||
|
'iCloud progress callback failed for $localId',
|
||||||
|
() => userCallbacks.onICloudProgress?.call(localId, progress),
|
||||||
|
),
|
||||||
|
onError: (localId, errorMessage) => _runUploadCallback(
|
||||||
|
'Upload error callback failed for $localId',
|
||||||
|
() => userCallbacks.onError?.call(localId, errorMessage),
|
||||||
|
),
|
||||||
|
onSuccess: (localId, remoteId) {
|
||||||
|
_runUploadCallback(
|
||||||
|
'Upload success callback failed for $localId',
|
||||||
|
() => userCallbacks.onSuccess?.call(localId, remoteId),
|
||||||
|
);
|
||||||
|
final source = localById[localId];
|
||||||
|
if (source == null) {
|
||||||
|
_logger.warning('Upload success for $localId but source LocalAsset missing; skipping album link');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAdds.add(
|
||||||
|
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||||
|
.then<void>((added) {
|
||||||
|
addedCount += added;
|
||||||
|
})
|
||||||
|
.catchError((Object error, StackTrace stack) {
|
||||||
|
_logger.warning('Failed to add uploaded asset $remoteId to album $albumId', error, stack);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks);
|
||||||
|
await Future.wait(pendingAdds);
|
||||||
|
return addedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _runUploadCallback(String message, void Function() callback) {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.warning(message, error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Links a freshly-uploaded asset to an album, ensuring the local DB
|
||||||
|
/// reflects the change without waiting for the next sync. We call the API
|
||||||
|
/// (server is the source of truth), then upsert a placeholder
|
||||||
|
/// `remote_asset_entity` row from the local source so the FK-protected
|
||||||
|
/// junction insert succeeds. Sync overwrites the placeholder later with
|
||||||
|
/// the authoritative server data.
|
||||||
|
Future<int> _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
|
||||||
|
final result = await _albumApiRepository.addAssets(albumId, [remoteId]);
|
||||||
|
if (result.added.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _repository.upsertRemoteAssetStub(remoteId: remoteId, ownerId: uploader.id, source: source);
|
||||||
|
await _repository.addAssets(albumId, result.added);
|
||||||
|
return result.added.length;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbum(String albumId) async {
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
await _albumApiRepository.deleteAlbum(albumId);
|
await _albumApiRepository.deleteAlbum(albumId);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum SyncMigrationTask {
|
|||||||
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
|
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
|
||||||
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
|
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
|
||||||
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
|
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
|
||||||
|
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncStreamService {
|
class SyncStreamService {
|
||||||
@@ -132,6 +133,13 @@ class SyncStreamService {
|
|||||||
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
|
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
|
||||||
|
semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
|
||||||
|
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
|
||||||
|
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
|
||||||
|
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _runPostSyncTasks(List<String> migrations) async {
|
Future<void> _runPostSyncTasks(List<String> migrations) async {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart';
|
||||||
|
|
||||||
|
final tagServiceProvider = Provider<TagService>((ref) => TagService(ref.watch(tagsApiRepositoryProvider)));
|
||||||
|
|
||||||
|
class TagService {
|
||||||
|
final TagsApiRepository _repository;
|
||||||
|
|
||||||
|
const TagService(this._repository);
|
||||||
|
|
||||||
|
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||||
|
return _repository.bulkTagAssets(assetIds, tagIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Tag>> getAllTags() async {
|
||||||
|
final dtos = await _repository.getAllTags();
|
||||||
|
if (dtos == null) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return dtos.map((dto) => Tag.fromDto(dto)).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Tag>> upsertTags(List<String> tags) async {
|
||||||
|
final dtos = await _repository.upsertTags(tags);
|
||||||
|
if (dtos == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return dtos.map((dto) => Tag.fromDto(dto)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
|
|
||||||
@@ -35,18 +34,21 @@ enum TimelineOrigin {
|
|||||||
deepLink,
|
deepLink,
|
||||||
albumActivities,
|
albumActivities,
|
||||||
folder,
|
folder,
|
||||||
|
recentlyAdded,
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineFactory {
|
class TimelineFactory {
|
||||||
final DriftTimelineRepository _timelineRepository;
|
final DriftTimelineRepository _timelineRepository;
|
||||||
final SettingsService _settingsService;
|
final MetadataRepository _metadataRepository;
|
||||||
|
|
||||||
const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService})
|
const TimelineFactory({
|
||||||
: _timelineRepository = timelineRepository,
|
required DriftTimelineRepository timelineRepository,
|
||||||
_settingsService = settingsService;
|
required MetadataRepository metadataRepository,
|
||||||
|
}) : _timelineRepository = timelineRepository,
|
||||||
|
_metadataRepository = metadataRepository;
|
||||||
|
|
||||||
GroupAssetsBy get groupBy {
|
GroupAssetsBy get groupBy {
|
||||||
final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
|
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
|
||||||
// We do not support auto grouping in the new timeline yet, fallback to day grouping
|
// We do not support auto grouping in the new timeline yet, fallback to day grouping
|
||||||
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
|
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,8 @@ class TimelineFactory {
|
|||||||
|
|
||||||
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
|
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
|
||||||
|
|
||||||
|
TimelineService recentlyAdded(String userId) => TimelineService(_timelineRepository.recentlyAdded(userId, groupBy));
|
||||||
|
|
||||||
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
|
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
|
||||||
|
|
||||||
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
|
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
createdAt: fileCreatedAt,
|
createdAt: fileCreatedAt,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
|
uploadedAt: createdAt,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
visibility: visibility.toAssetVisibility(),
|
visibility: visibility.toAssetVisibility(),
|
||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
@@ -33,6 +34,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
createdAt: fileCreatedAt,
|
createdAt: fileCreatedAt,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
|
uploadedAt: createdAt,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
visibility: visibility.toAssetVisibility(),
|
visibility: visibility.toAssetVisibility(),
|
||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ extension StringExtension on String {
|
|||||||
String capitalize() {
|
String capitalize() {
|
||||||
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
|
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get nullIfEmpty => isEmpty ? null : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DurationExtension on String {
|
extension DurationExtension on String {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|||||||
|
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
|
||||||
|
@TableIndex.sql('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person
|
||||||
|
ON asset_face_entity (person_id, asset_id)
|
||||||
|
WHERE is_visible = 1 AND deleted_at IS NULL
|
||||||
|
''')
|
||||||
class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
||||||
const AssetFaceEntity();
|
const AssetFaceEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -1350,3 +1350,7 @@ i0.Index get idxAssetFaceAssetId => i0.Index(
|
|||||||
'idx_asset_face_asset_id',
|
'idx_asset_face_asset_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
);
|
);
|
||||||
|
i0.Index get idxAssetFaceVisiblePerson => i0.Index(
|
||||||
|
'idx_asset_face_visible_person',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
|
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
|
||||||
|
@TableIndex.sql('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_remote_exif_city
|
||||||
|
ON remote_exif_entity (city) WHERE city IS NOT NULL
|
||||||
|
''')
|
||||||
class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
||||||
const RemoteExifEntity();
|
const RemoteExifEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -1883,3 +1883,8 @@ class RemoteExifEntityCompanion
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i0.Index get idxRemoteExifCity => i0.Index(
|
||||||
|
'idx_remote_exif_city',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'local_asset.entity.dart';
|
|||||||
import 'local_album.entity.dart';
|
import 'local_album.entity.dart';
|
||||||
import 'local_album_asset.entity.dart';
|
import 'local_album_asset.entity.dart';
|
||||||
|
|
||||||
mergedAsset:
|
mergedAsset:
|
||||||
SELECT
|
SELECT
|
||||||
rae.id as remote_id,
|
rae.id as remote_id,
|
||||||
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
|
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
|
||||||
@@ -27,7 +27,8 @@ SELECT
|
|||||||
NULL as longitude,
|
NULL as longitude,
|
||||||
NULL as adjustmentTime,
|
NULL as adjustmentTime,
|
||||||
rae.is_edited,
|
rae.is_edited,
|
||||||
0 as playback_style
|
0 as playback_style,
|
||||||
|
rae.uploaded_at
|
||||||
FROM
|
FROM
|
||||||
remote_asset_entity rae
|
remote_asset_entity rae
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -65,7 +66,8 @@ SELECT
|
|||||||
lae.longitude,
|
lae.longitude,
|
||||||
lae.adjustment_time,
|
lae.adjustment_time,
|
||||||
0 as is_edited,
|
0 as is_edited,
|
||||||
lae.playback_style
|
lae.playback_style,
|
||||||
|
NULL as uploaded_at
|
||||||
FROM
|
FROM
|
||||||
local_asset_entity lae
|
local_asset_entity lae
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
|
|||||||
+4
-1
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
);
|
);
|
||||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae 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 ($expandeduserIds) 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 AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, 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, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae 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 ($expandeduserIds) 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 AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, 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, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||||
variables: [
|
variables: [
|
||||||
for (var $ in userIds) i0.Variable<String>($),
|
for (var $ in userIds) i0.Variable<String>($),
|
||||||
...generatedlimit.introducedVariables,
|
...generatedlimit.introducedVariables,
|
||||||
@@ -68,6 +68,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
||||||
isEdited: row.read<bool>('is_edited'),
|
isEdited: row.read<bool>('is_edited'),
|
||||||
playbackStyle: row.read<int>('playback_style'),
|
playbackStyle: row.read<int>('playback_style'),
|
||||||
|
uploadedAt: row.readNullable<DateTime>('uploaded_at'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -141,6 +142,7 @@ class MergedAssetResult {
|
|||||||
final DateTime? adjustmentTime;
|
final DateTime? adjustmentTime;
|
||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
final int playbackStyle;
|
final int playbackStyle;
|
||||||
|
final DateTime? uploadedAt;
|
||||||
MergedAssetResult({
|
MergedAssetResult({
|
||||||
this.remoteId,
|
this.remoteId,
|
||||||
this.localId,
|
this.localId,
|
||||||
@@ -164,6 +166,7 @@ class MergedAssetResult {
|
|||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
required this.isEdited,
|
required this.isEdited,
|
||||||
required this.playbackStyle,
|
required this.playbackStyle,
|
||||||
|
this.uploadedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
@TableIndex.sql(
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
|
||||||
)
|
|
||||||
@TableIndex.sql('''
|
@TableIndex.sql('''
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum
|
CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum
|
||||||
ON remote_asset_entity (owner_id, checksum)
|
ON remote_asset_entity (owner_id, checksum)
|
||||||
@@ -20,12 +17,10 @@ WHERE (library_id IS NOT NULL);
|
|||||||
''')
|
''')
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
|
||||||
@TableIndex.sql(
|
@TableIndex.sql('''
|
||||||
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
|
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
||||||
)
|
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
||||||
@TableIndex.sql(
|
''')
|
||||||
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
|
|
||||||
)
|
|
||||||
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
const RemoteAssetEntity();
|
const RemoteAssetEntity();
|
||||||
|
|
||||||
@@ -43,6 +38,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
|||||||
|
|
||||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
DateTimeColumn get uploadedAt => dateTime().nullable()();
|
||||||
|
|
||||||
TextColumn get livePhotoVideoId => text().nullable()();
|
TextColumn get livePhotoVideoId => text().nullable()();
|
||||||
|
|
||||||
IntColumn get visibility => intEnum<AssetVisibility>()();
|
IntColumn get visibility => intEnum<AssetVisibility>()();
|
||||||
@@ -66,6 +63,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
type: type,
|
type: type,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
|
uploadedAt: uploadedAt,
|
||||||
durationMs: durationMs,
|
durationMs: durationMs,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -76,5 +74,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
localId: localId,
|
localId: localId,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
isEdited: isEdited,
|
isEdited: isEdited,
|
||||||
|
deletedAt: deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-14
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
|
|||||||
i0.Value<DateTime?> localDateTime,
|
i0.Value<DateTime?> localDateTime,
|
||||||
i0.Value<String?> thumbHash,
|
i0.Value<String?> thumbHash,
|
||||||
i0.Value<DateTime?> deletedAt,
|
i0.Value<DateTime?> deletedAt,
|
||||||
|
i0.Value<DateTime?> uploadedAt,
|
||||||
i0.Value<String?> livePhotoVideoId,
|
i0.Value<String?> livePhotoVideoId,
|
||||||
required i2.AssetVisibility visibility,
|
required i2.AssetVisibility visibility,
|
||||||
i0.Value<String?> stackId,
|
i0.Value<String?> stackId,
|
||||||
@@ -49,6 +50,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
|
|||||||
i0.Value<DateTime?> localDateTime,
|
i0.Value<DateTime?> localDateTime,
|
||||||
i0.Value<String?> thumbHash,
|
i0.Value<String?> thumbHash,
|
||||||
i0.Value<DateTime?> deletedAt,
|
i0.Value<DateTime?> deletedAt,
|
||||||
|
i0.Value<DateTime?> uploadedAt,
|
||||||
i0.Value<String?> livePhotoVideoId,
|
i0.Value<String?> livePhotoVideoId,
|
||||||
i0.Value<i2.AssetVisibility> visibility,
|
i0.Value<i2.AssetVisibility> visibility,
|
||||||
i0.Value<String?> stackId,
|
i0.Value<String?> stackId,
|
||||||
@@ -177,6 +179,11 @@ class $$RemoteAssetEntityTableFilterComposer
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get uploadedAt => $composableBuilder(
|
||||||
|
column: $table.uploadedAt,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
|
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
|
||||||
column: $table.livePhotoVideoId,
|
column: $table.livePhotoVideoId,
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
@@ -305,6 +312,11 @@ class $$RemoteAssetEntityTableOrderingComposer
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get uploadedAt => $composableBuilder(
|
||||||
|
column: $table.uploadedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
|
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
|
||||||
column: $table.livePhotoVideoId,
|
column: $table.livePhotoVideoId,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
@@ -412,6 +424,11 @@ class $$RemoteAssetEntityTableAnnotationComposer
|
|||||||
i0.GeneratedColumn<DateTime> get deletedAt =>
|
i0.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
|
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get uploadedAt => $composableBuilder(
|
||||||
|
column: $table.uploadedAt,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
|
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
|
||||||
column: $table.livePhotoVideoId,
|
column: $table.livePhotoVideoId,
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
@@ -507,6 +524,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
||||||
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<DateTime?> uploadedAt = 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 =
|
i0.Value<i2.AssetVisibility> visibility =
|
||||||
const i0.Value.absent(),
|
const i0.Value.absent(),
|
||||||
@@ -528,6 +546,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
localDateTime: localDateTime,
|
localDateTime: localDateTime,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
|
uploadedAt: uploadedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
@@ -550,6 +569,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
||||||
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<DateTime?> uploadedAt = 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(),
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
@@ -570,6 +590,7 @@ class $$RemoteAssetEntityTableTableManager
|
|||||||
localDateTime: localDateTime,
|
localDateTime: localDateTime,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
|
uploadedAt: uploadedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
@@ -645,9 +666,9 @@ typedef $$RemoteAssetEntityTableProcessedTableManager =
|
|||||||
i1.RemoteAssetEntityData,
|
i1.RemoteAssetEntityData,
|
||||||
i0.PrefetchHooks Function({bool ownerId})
|
i0.PrefetchHooks Function({bool ownerId})
|
||||||
>;
|
>;
|
||||||
i0.Index get idxRemoteAssetOwnerChecksum => i0.Index(
|
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
|
||||||
'idx_remote_asset_owner_checksum',
|
'UQ_remote_assets_owner_checksum',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
);
|
);
|
||||||
|
|
||||||
class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
||||||
@@ -818,6 +839,18 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
type: i0.DriftSqlType.dateTime,
|
type: i0.DriftSqlType.dateTime,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _uploadedAtMeta = const i0.VerificationMeta(
|
||||||
|
'uploadedAt',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> uploadedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>(
|
||||||
|
'uploaded_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
static const i0.VerificationMeta _livePhotoVideoIdMeta =
|
static const i0.VerificationMeta _livePhotoVideoIdMeta =
|
||||||
const i0.VerificationMeta('livePhotoVideoId');
|
const i0.VerificationMeta('livePhotoVideoId');
|
||||||
@override
|
@override
|
||||||
@@ -894,6 +927,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
localDateTime,
|
localDateTime,
|
||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
|
uploadedAt,
|
||||||
livePhotoVideoId,
|
livePhotoVideoId,
|
||||||
visibility,
|
visibility,
|
||||||
stackId,
|
stackId,
|
||||||
@@ -998,6 +1032,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
|
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('uploaded_at')) {
|
||||||
|
context.handle(
|
||||||
|
_uploadedAtMeta,
|
||||||
|
uploadedAt.isAcceptableOrUnknown(data['uploaded_at']!, _uploadedAtMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('live_photo_video_id')) {
|
if (data.containsKey('live_photo_video_id')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_livePhotoVideoIdMeta,
|
_livePhotoVideoIdMeta,
|
||||||
@@ -1095,6 +1135,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
i0.DriftSqlType.dateTime,
|
i0.DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}deleted_at'],
|
data['${effectivePrefix}deleted_at'],
|
||||||
),
|
),
|
||||||
|
uploadedAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime,
|
||||||
|
data['${effectivePrefix}uploaded_at'],
|
||||||
|
),
|
||||||
livePhotoVideoId: attachedDatabase.typeMapping.read(
|
livePhotoVideoId: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}live_photo_video_id'],
|
data['${effectivePrefix}live_photo_video_id'],
|
||||||
@@ -1153,6 +1197,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
final DateTime? localDateTime;
|
final DateTime? localDateTime;
|
||||||
final String? thumbHash;
|
final String? thumbHash;
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
|
final DateTime? uploadedAt;
|
||||||
final String? livePhotoVideoId;
|
final String? livePhotoVideoId;
|
||||||
final i2.AssetVisibility visibility;
|
final i2.AssetVisibility visibility;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
@@ -1173,6 +1218,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
this.localDateTime,
|
this.localDateTime,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
|
this.uploadedAt,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
required this.visibility,
|
required this.visibility,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
@@ -1212,6 +1258,9 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
if (!nullToAbsent || deletedAt != null) {
|
if (!nullToAbsent || deletedAt != null) {
|
||||||
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || uploadedAt != null) {
|
||||||
|
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt);
|
||||||
|
}
|
||||||
if (!nullToAbsent || livePhotoVideoId != null) {
|
if (!nullToAbsent || livePhotoVideoId != null) {
|
||||||
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
|
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
|
||||||
}
|
}
|
||||||
@@ -1252,6 +1301,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
|
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
|
||||||
thumbHash: serializer.fromJson<String?>(json['thumbHash']),
|
thumbHash: serializer.fromJson<String?>(json['thumbHash']),
|
||||||
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
||||||
|
uploadedAt: serializer.fromJson<DateTime?>(json['uploadedAt']),
|
||||||
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
|
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
|
||||||
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromJson(
|
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromJson(
|
||||||
serializer.fromJson<int>(json['visibility']),
|
serializer.fromJson<int>(json['visibility']),
|
||||||
@@ -1281,6 +1331,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
'localDateTime': serializer.toJson<DateTime?>(localDateTime),
|
'localDateTime': serializer.toJson<DateTime?>(localDateTime),
|
||||||
'thumbHash': serializer.toJson<String?>(thumbHash),
|
'thumbHash': serializer.toJson<String?>(thumbHash),
|
||||||
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
||||||
|
'uploadedAt': serializer.toJson<DateTime?>(uploadedAt),
|
||||||
'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),
|
||||||
@@ -1306,6 +1357,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
|
||||||
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<DateTime?> uploadedAt = 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(),
|
i0.Value<String?> stackId = const i0.Value.absent(),
|
||||||
@@ -1328,6 +1380,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
: this.localDateTime,
|
: this.localDateTime,
|
||||||
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
|
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
|
||||||
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
||||||
|
uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId.present
|
livePhotoVideoId: livePhotoVideoId.present
|
||||||
? livePhotoVideoId.value
|
? livePhotoVideoId.value
|
||||||
: this.livePhotoVideoId,
|
: this.livePhotoVideoId,
|
||||||
@@ -1358,6 +1411,9 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
: this.localDateTime,
|
: this.localDateTime,
|
||||||
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
|
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
|
||||||
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
||||||
|
uploadedAt: data.uploadedAt.present
|
||||||
|
? data.uploadedAt.value
|
||||||
|
: this.uploadedAt,
|
||||||
livePhotoVideoId: data.livePhotoVideoId.present
|
livePhotoVideoId: data.livePhotoVideoId.present
|
||||||
? data.livePhotoVideoId.value
|
? data.livePhotoVideoId.value
|
||||||
: this.livePhotoVideoId,
|
: this.livePhotoVideoId,
|
||||||
@@ -1387,6 +1443,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
..write('localDateTime: $localDateTime, ')
|
..write('localDateTime: $localDateTime, ')
|
||||||
..write('thumbHash: $thumbHash, ')
|
..write('thumbHash: $thumbHash, ')
|
||||||
..write('deletedAt: $deletedAt, ')
|
..write('deletedAt: $deletedAt, ')
|
||||||
|
..write('uploadedAt: $uploadedAt, ')
|
||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility, ')
|
..write('visibility: $visibility, ')
|
||||||
..write('stackId: $stackId, ')
|
..write('stackId: $stackId, ')
|
||||||
@@ -1412,6 +1469,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
localDateTime,
|
localDateTime,
|
||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
|
uploadedAt,
|
||||||
livePhotoVideoId,
|
livePhotoVideoId,
|
||||||
visibility,
|
visibility,
|
||||||
stackId,
|
stackId,
|
||||||
@@ -1436,6 +1494,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
other.localDateTime == this.localDateTime &&
|
other.localDateTime == this.localDateTime &&
|
||||||
other.thumbHash == this.thumbHash &&
|
other.thumbHash == this.thumbHash &&
|
||||||
other.deletedAt == this.deletedAt &&
|
other.deletedAt == this.deletedAt &&
|
||||||
|
other.uploadedAt == this.uploadedAt &&
|
||||||
other.livePhotoVideoId == this.livePhotoVideoId &&
|
other.livePhotoVideoId == this.livePhotoVideoId &&
|
||||||
other.visibility == this.visibility &&
|
other.visibility == this.visibility &&
|
||||||
other.stackId == this.stackId &&
|
other.stackId == this.stackId &&
|
||||||
@@ -1459,6 +1518,7 @@ class RemoteAssetEntityCompanion
|
|||||||
final i0.Value<DateTime?> localDateTime;
|
final i0.Value<DateTime?> localDateTime;
|
||||||
final i0.Value<String?> thumbHash;
|
final i0.Value<String?> thumbHash;
|
||||||
final i0.Value<DateTime?> deletedAt;
|
final i0.Value<DateTime?> deletedAt;
|
||||||
|
final i0.Value<DateTime?> uploadedAt;
|
||||||
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;
|
final i0.Value<String?> stackId;
|
||||||
@@ -1479,6 +1539,7 @@ class RemoteAssetEntityCompanion
|
|||||||
this.localDateTime = const i0.Value.absent(),
|
this.localDateTime = const i0.Value.absent(),
|
||||||
this.thumbHash = const i0.Value.absent(),
|
this.thumbHash = const i0.Value.absent(),
|
||||||
this.deletedAt = const i0.Value.absent(),
|
this.deletedAt = const i0.Value.absent(),
|
||||||
|
this.uploadedAt = 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(),
|
this.stackId = const i0.Value.absent(),
|
||||||
@@ -1500,6 +1561,7 @@ class RemoteAssetEntityCompanion
|
|||||||
this.localDateTime = const i0.Value.absent(),
|
this.localDateTime = const i0.Value.absent(),
|
||||||
this.thumbHash = const i0.Value.absent(),
|
this.thumbHash = const i0.Value.absent(),
|
||||||
this.deletedAt = const i0.Value.absent(),
|
this.deletedAt = const i0.Value.absent(),
|
||||||
|
this.uploadedAt = 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(),
|
this.stackId = const i0.Value.absent(),
|
||||||
@@ -1526,6 +1588,7 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Expression<DateTime>? localDateTime,
|
i0.Expression<DateTime>? localDateTime,
|
||||||
i0.Expression<String>? thumbHash,
|
i0.Expression<String>? thumbHash,
|
||||||
i0.Expression<DateTime>? deletedAt,
|
i0.Expression<DateTime>? deletedAt,
|
||||||
|
i0.Expression<DateTime>? uploadedAt,
|
||||||
i0.Expression<String>? livePhotoVideoId,
|
i0.Expression<String>? livePhotoVideoId,
|
||||||
i0.Expression<int>? visibility,
|
i0.Expression<int>? visibility,
|
||||||
i0.Expression<String>? stackId,
|
i0.Expression<String>? stackId,
|
||||||
@@ -1547,6 +1610,7 @@ class RemoteAssetEntityCompanion
|
|||||||
if (localDateTime != null) 'local_date_time': localDateTime,
|
if (localDateTime != null) 'local_date_time': localDateTime,
|
||||||
if (thumbHash != null) 'thumb_hash': thumbHash,
|
if (thumbHash != null) 'thumb_hash': thumbHash,
|
||||||
if (deletedAt != null) 'deleted_at': deletedAt,
|
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||||
|
if (uploadedAt != null) 'uploaded_at': uploadedAt,
|
||||||
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,
|
if (stackId != null) 'stack_id': stackId,
|
||||||
@@ -1570,6 +1634,7 @@ class RemoteAssetEntityCompanion
|
|||||||
i0.Value<DateTime?>? localDateTime,
|
i0.Value<DateTime?>? localDateTime,
|
||||||
i0.Value<String?>? thumbHash,
|
i0.Value<String?>? thumbHash,
|
||||||
i0.Value<DateTime?>? deletedAt,
|
i0.Value<DateTime?>? deletedAt,
|
||||||
|
i0.Value<DateTime?>? uploadedAt,
|
||||||
i0.Value<String?>? livePhotoVideoId,
|
i0.Value<String?>? livePhotoVideoId,
|
||||||
i0.Value<i2.AssetVisibility>? visibility,
|
i0.Value<i2.AssetVisibility>? visibility,
|
||||||
i0.Value<String?>? stackId,
|
i0.Value<String?>? stackId,
|
||||||
@@ -1591,6 +1656,7 @@ class RemoteAssetEntityCompanion
|
|||||||
localDateTime: localDateTime ?? this.localDateTime,
|
localDateTime: localDateTime ?? this.localDateTime,
|
||||||
thumbHash: thumbHash ?? this.thumbHash,
|
thumbHash: thumbHash ?? this.thumbHash,
|
||||||
deletedAt: deletedAt ?? this.deletedAt,
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
uploadedAt: uploadedAt ?? this.uploadedAt,
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
stackId: stackId ?? this.stackId,
|
stackId: stackId ?? this.stackId,
|
||||||
@@ -1646,6 +1712,9 @@ class RemoteAssetEntityCompanion
|
|||||||
if (deletedAt.present) {
|
if (deletedAt.present) {
|
||||||
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
|
||||||
}
|
}
|
||||||
|
if (uploadedAt.present) {
|
||||||
|
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt.value);
|
||||||
|
}
|
||||||
if (livePhotoVideoId.present) {
|
if (livePhotoVideoId.present) {
|
||||||
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
|
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
|
||||||
}
|
}
|
||||||
@@ -1683,6 +1752,7 @@ class RemoteAssetEntityCompanion
|
|||||||
..write('localDateTime: $localDateTime, ')
|
..write('localDateTime: $localDateTime, ')
|
||||||
..write('thumbHash: $thumbHash, ')
|
..write('thumbHash: $thumbHash, ')
|
||||||
..write('deletedAt: $deletedAt, ')
|
..write('deletedAt: $deletedAt, ')
|
||||||
|
..write('uploadedAt: $uploadedAt, ')
|
||||||
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
..write('livePhotoVideoId: $livePhotoVideoId, ')
|
||||||
..write('visibility: $visibility, ')
|
..write('visibility: $visibility, ')
|
||||||
..write('stackId: $stackId, ')
|
..write('stackId: $stackId, ')
|
||||||
@@ -1693,10 +1763,6 @@ class RemoteAssetEntityCompanion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
|
|
||||||
'UQ_remote_assets_owner_checksum',
|
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
|
||||||
);
|
|
||||||
i0.Index get uQRemoteAssetsOwnerLibraryChecksum => i0.Index(
|
i0.Index get uQRemoteAssetsOwnerLibraryChecksum => i0.Index(
|
||||||
'UQ_remote_assets_owner_library_checksum',
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
@@ -1709,11 +1775,7 @@ i0.Index get idxRemoteAssetStackId => i0.Index(
|
|||||||
'idx_remote_asset_stack_id',
|
'idx_remote_asset_stack_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
);
|
);
|
||||||
i0.Index get idxRemoteAssetLocalDateTimeDay => i0.Index(
|
i0.Index get idxRemoteAssetOwnerVisibilityDeletedCreated => i0.Index(
|
||||||
'idx_remote_asset_local_date_time_day',
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
);
|
|
||||||
i0.Index get idxRemoteAssetLocalDateTimeMonth => i0.Index(
|
|
||||||
'idx_remote_asset_local_date_time_month',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class Drift extends $Drift {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 25;
|
int get schemaVersion => 26;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -266,6 +266,15 @@ class Drift extends $Drift {
|
|||||||
},
|
},
|
||||||
from24To25: (m, v25) async {
|
from24To25: (m, v25) async {
|
||||||
await m.createTable(v25.metadata);
|
await m.createTable(v25.metadata);
|
||||||
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
||||||
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
||||||
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
||||||
|
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
||||||
|
await m.createIndex(v25.idxRemoteExifCity);
|
||||||
|
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
||||||
|
},
|
||||||
|
from25To26: (m, v26) async {
|
||||||
|
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -113,13 +113,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i4.idxLocalAssetChecksum,
|
i4.idxLocalAssetChecksum,
|
||||||
i4.idxLocalAssetCloudId,
|
i4.idxLocalAssetCloudId,
|
||||||
i3.idxStackPrimaryAssetId,
|
i3.idxStackPrimaryAssetId,
|
||||||
i2.idxRemoteAssetOwnerChecksum,
|
|
||||||
i2.uQRemoteAssetsOwnerChecksum,
|
i2.uQRemoteAssetsOwnerChecksum,
|
||||||
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
i2.idxRemoteAssetChecksum,
|
i2.idxRemoteAssetChecksum,
|
||||||
i2.idxRemoteAssetStackId,
|
i2.idxRemoteAssetStackId,
|
||||||
i2.idxRemoteAssetLocalDateTimeDay,
|
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
i2.idxRemoteAssetLocalDateTimeMonth,
|
|
||||||
authUserEntity,
|
authUserEntity,
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
@@ -137,11 +135,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
metadataEntity,
|
metadataEntity,
|
||||||
i10.idxPartnerSharedWithId,
|
i10.idxPartnerSharedWithId,
|
||||||
i11.idxLatLng,
|
i11.idxLatLng,
|
||||||
|
i11.idxRemoteExifCity,
|
||||||
i12.idxRemoteAlbumAssetAlbumAsset,
|
i12.idxRemoteAlbumAssetAlbumAsset,
|
||||||
i14.idxRemoteAssetCloudId,
|
i14.idxRemoteAssetCloudId,
|
||||||
i17.idxPersonOwnerId,
|
i17.idxPersonOwnerId,
|
||||||
i18.idxAssetFacePersonId,
|
i18.idxAssetFacePersonId,
|
||||||
i18.idxAssetFaceAssetId,
|
i18.idxAssetFaceAssetId,
|
||||||
|
i18.idxAssetFaceVisiblePerson,
|
||||||
i20.idxTrashedLocalAssetChecksum,
|
i20.idxTrashedLocalAssetChecksum,
|
||||||
i20.idxTrashedLocalAssetAlbum,
|
i20.idxTrashedLocalAssetAlbum,
|
||||||
i21.idxAssetEditAssetId,
|
i21.idxAssetEditAssetId,
|
||||||
|
|||||||
+618
-14
@@ -12390,13 +12390,11 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
idxLocalAssetChecksum,
|
idxLocalAssetChecksum,
|
||||||
idxLocalAssetCloudId,
|
idxLocalAssetCloudId,
|
||||||
idxStackPrimaryAssetId,
|
idxStackPrimaryAssetId,
|
||||||
idxRemoteAssetOwnerChecksum,
|
|
||||||
uQRemoteAssetsOwnerChecksum,
|
uQRemoteAssetsOwnerChecksum,
|
||||||
uQRemoteAssetsOwnerLibraryChecksum,
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
idxRemoteAssetChecksum,
|
idxRemoteAssetChecksum,
|
||||||
idxRemoteAssetStackId,
|
idxRemoteAssetStackId,
|
||||||
idxRemoteAssetLocalDateTimeDay,
|
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
idxRemoteAssetLocalDateTimeMonth,
|
|
||||||
authUserEntity,
|
authUserEntity,
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
@@ -12414,11 +12412,13 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
metadata,
|
metadata,
|
||||||
idxPartnerSharedWithId,
|
idxPartnerSharedWithId,
|
||||||
idxLatLng,
|
idxLatLng,
|
||||||
|
idxRemoteExifCity,
|
||||||
idxRemoteAlbumAssetAlbumAsset,
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
idxRemoteAssetCloudId,
|
idxRemoteAssetCloudId,
|
||||||
idxPersonOwnerId,
|
idxPersonOwnerId,
|
||||||
idxAssetFacePersonId,
|
idxAssetFacePersonId,
|
||||||
idxAssetFaceAssetId,
|
idxAssetFaceAssetId,
|
||||||
|
idxAssetFaceVisiblePerson,
|
||||||
idxTrashedLocalAssetChecksum,
|
idxTrashedLocalAssetChecksum,
|
||||||
idxTrashedLocalAssetAlbum,
|
idxTrashedLocalAssetAlbum,
|
||||||
idxAssetEditAssetId,
|
idxAssetEditAssetId,
|
||||||
@@ -12583,10 +12583,6 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
'idx_stack_primary_asset_id',
|
'idx_stack_primary_asset_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
);
|
);
|
||||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
|
||||||
'idx_remote_asset_owner_checksum',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
|
||||||
);
|
|
||||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
'UQ_remote_assets_owner_checksum',
|
'UQ_remote_assets_owner_checksum',
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
@@ -12603,13 +12599,9 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
'idx_remote_asset_stack_id',
|
'idx_remote_asset_stack_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
);
|
);
|
||||||
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||||
'idx_remote_asset_local_date_time_day',
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
);
|
|
||||||
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
|
||||||
'idx_remote_asset_local_date_time_month',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
|
||||||
);
|
);
|
||||||
late final Shape40 authUserEntity = Shape40(
|
late final Shape40 authUserEntity = Shape40(
|
||||||
source: i0.VersionedTable(
|
source: i0.VersionedTable(
|
||||||
@@ -12883,6 +12875,10 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
'idx_lat_lng',
|
'idx_lat_lng',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
);
|
);
|
||||||
|
final i1.Index idxRemoteExifCity = i1.Index(
|
||||||
|
'idx_remote_exif_city',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||||
|
);
|
||||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
'idx_remote_album_asset_album_asset',
|
'idx_remote_album_asset_album_asset',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
@@ -12903,6 +12899,10 @@ final class Schema25 extends i0.VersionedSchema {
|
|||||||
'idx_asset_face_asset_id',
|
'idx_asset_face_asset_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
);
|
);
|
||||||
|
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||||
|
'idx_asset_face_visible_person',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||||
|
);
|
||||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
'idx_trashed_local_asset_checksum',
|
'idx_trashed_local_asset_checksum',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
@@ -12943,6 +12943,602 @@ i1.GeneratedColumn<String> _column_211(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
$customConstraints: 'NOT NULL',
|
$customConstraints: 'NOT NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema26 extends i0.VersionedSchema {
|
||||||
|
Schema26({required super.database}) : super(version: 26);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAlbumAssetAlbumAsset,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxStackPrimaryAssetId,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
idxRemoteAssetStackId,
|
||||||
|
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
assetEditEntity,
|
||||||
|
metadata,
|
||||||
|
idxPartnerSharedWithId,
|
||||||
|
idxLatLng,
|
||||||
|
idxRemoteExifCity,
|
||||||
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAssetCloudId,
|
||||||
|
idxPersonOwnerId,
|
||||||
|
idxAssetFacePersonId,
|
||||||
|
idxAssetFaceAssetId,
|
||||||
|
idxAssetFaceVisiblePerson,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
idxAssetEditAssetId,
|
||||||
|
];
|
||||||
|
late final Shape33 userEntity = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 remoteAssetEntity = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_212,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 stackEntity = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_130,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape36 localAssetEntity = Shape36(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 remoteAlbumEntity = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_138,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 localAlbumEntity = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_115,
|
||||||
|
_column_142,
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_146, _column_147, _column_145],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_local_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||||
|
'idx_stack_primary_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||||
|
'idx_remote_asset_stack_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||||
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
|
);
|
||||||
|
late final Shape40 authUserEntity = Shape40(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_148,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_149,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_153, _column_154, _column_155],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 partnerEntity = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_156, _column_157, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape42 remoteExifEntity = Shape42(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_117,
|
||||||
|
_column_116,
|
||||||
|
_column_165,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_169,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_176,
|
||||||
|
],
|
||||||
|
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_159, _column_177],
|
||||||
|
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_177, _column_153, _column_178],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 memoryEntity = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_124,
|
||||||
|
_column_121,
|
||||||
|
_column_113,
|
||||||
|
_column_181,
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
],
|
||||||
|
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_159, _column_187],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 personEntity = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_108,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_192,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 assetFaceEntity = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_193,
|
||||||
|
_column_194,
|
||||||
|
_column_195,
|
||||||
|
_column_196,
|
||||||
|
_column_197,
|
||||||
|
_column_198,
|
||||||
|
_column_199,
|
||||||
|
_column_200,
|
||||||
|
_column_201,
|
||||||
|
_column_124,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_202, _column_203, _column_204],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_205,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_206,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 assetEditEntity = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_edit_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_207,
|
||||||
|
_column_208,
|
||||||
|
_column_209,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape49 metadata = Shape49(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'metadata',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY("key")'],
|
||||||
|
columns: [_column_210, _column_211, _column_115],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||||
|
'idx_partner_shared_with_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteExifCity = i1.Index(
|
||||||
|
'idx_remote_exif_city',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_remote_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||||
|
'idx_remote_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxPersonOwnerId = i1.Index(
|
||||||
|
'idx_person_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||||
|
'idx_asset_face_person_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||||
|
'idx_asset_face_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||||
|
'idx_asset_face_visible_person',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||||
|
'idx_asset_edit_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape50 extends i0.VersionedTable {
|
||||||
|
Shape50({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<String> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
|
||||||
|
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 durationMs =>
|
||||||
|
columnsByName['duration_ms']! 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<int> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get localDateTime =>
|
||||||
|
columnsByName['local_date_time']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get thumbHash =>
|
||||||
|
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get uploadedAt =>
|
||||||
|
columnsByName['uploaded_at']! as i1.GeneratedColumn<String>;
|
||||||
|
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<String> get libraryId =>
|
||||||
|
columnsByName['library_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get isEdited =>
|
||||||
|
columnsByName['is_edited']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_212(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'uploaded_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
$customConstraints: 'NULL',
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -12968,6 +13564,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||||
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -13091,6 +13688,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from24To25(migrator, schema);
|
await from24To25(migrator, schema);
|
||||||
return 25;
|
return 25;
|
||||||
|
case 25:
|
||||||
|
final schema = Schema26(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from25To26(migrator, schema);
|
||||||
|
return 26;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -13122,6 +13724,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||||
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -13148,5 +13751,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from22To23: from22To23,
|
from22To23: from22To23,
|
||||||
from23To24: from23To24,
|
from23To24: from23To24,
|
||||||
from24To25: from24To25,
|
from24To25: from24To25,
|
||||||
|
from25To26: from25To26,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
@@ -120,9 +121,49 @@ extension<T extends Object> on MetadataDomain<T> {
|
|||||||
cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo),
|
cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo),
|
||||||
defaultsInitialized: repo._read(.cleanupDefaultsInitialized),
|
defaultsInitialized: repo._read(.cleanupDefaultsInitialized),
|
||||||
),
|
),
|
||||||
|
map: .new(
|
||||||
|
relativeDays: repo._read(.mapRelativeDate),
|
||||||
|
favoritesOnly: repo._read(.mapShowFavoriteOnly),
|
||||||
|
includeArchived: repo._read(.mapIncludeArchived),
|
||||||
|
themeMode: repo._read(.mapThemeMode),
|
||||||
|
withPartners: repo._read(.mapWithPartners),
|
||||||
|
),
|
||||||
|
timeline: .new(
|
||||||
|
tilesPerRow: repo._read(.timelineTilesPerRow),
|
||||||
|
groupAssetsBy: repo._read(.timelineGroupAssetsBy),
|
||||||
|
storageIndicator: repo._read(.timelineStorageIndicator),
|
||||||
|
),
|
||||||
|
image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)),
|
||||||
|
viewer: .new(
|
||||||
|
loopVideo: repo._read(.viewerLoopVideo),
|
||||||
|
loadOriginalVideo: repo._read(.viewerLoadOriginalVideo),
|
||||||
|
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
|
||||||
|
tapToNavigate: repo._read(.viewerTapToNavigate),
|
||||||
|
),
|
||||||
|
slideshow: .new(
|
||||||
|
transition: repo._read(.slideshowTransition),
|
||||||
|
repeat: repo._read(.slideshowRepeat),
|
||||||
|
duration: repo._read(.slideshowDuration),
|
||||||
|
look: repo._read(.slideshowLook),
|
||||||
|
direction: repo._read(.slideshowDirection),
|
||||||
|
),
|
||||||
|
album: .new(
|
||||||
|
sortMode: repo._read(.albumSortMode),
|
||||||
|
isReverse: repo._read(.albumIsReverse),
|
||||||
|
isGrid: repo._read(.albumIsGrid),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case .systemConfig:
|
case .systemConfig:
|
||||||
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
repo._systemConfig = .new(
|
||||||
|
logLevel: repo._read(.logLevel),
|
||||||
|
network: .new(
|
||||||
|
autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching),
|
||||||
|
preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty,
|
||||||
|
localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty,
|
||||||
|
externalEndpointList: repo._read(.networkExternalEndpointList),
|
||||||
|
customHeaders: repo._read(.networkCustomHeaders),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.
|
|||||||
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';
|
||||||
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/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
enum SortRemoteAlbumsBy { id, updatedAt }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
@@ -159,7 +160,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
createdAt: Value(album.createdAt),
|
createdAt: Value(album.createdAt),
|
||||||
updatedAt: Value(album.updatedAt),
|
updatedAt: Value(album.updatedAt),
|
||||||
description: Value(album.description),
|
description: Value(album.description),
|
||||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
thumbnailAssetId: Value(album.thumbnailAssetId ?? (assetIds.isNotEmpty ? assetIds.first : null)),
|
||||||
isActivityEnabled: Value(album.isActivityEnabled),
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
order: Value(album.order),
|
order: Value(album.order),
|
||||||
);
|
);
|
||||||
@@ -274,17 +275,59 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
||||||
|
if (assetIds.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
final albumAssets = assetIds.map(
|
final albumAssets = assetIds.map(
|
||||||
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(albumId), assetId: Value(assetId)),
|
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(albumId), assetId: Value(assetId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _db.batch((batch) {
|
await _db.transaction(() async {
|
||||||
batch.insertAll(_db.remoteAlbumAssetEntity, albumAssets);
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(_db.remoteAlbumAssetEntity, albumAssets);
|
||||||
|
});
|
||||||
|
|
||||||
|
final album = _db.update(_db.remoteAlbumEntity)
|
||||||
|
..where((row) => row.id.equals(albumId) & row.thumbnailAssetId.isNull());
|
||||||
|
|
||||||
|
await album.write(RemoteAlbumEntityCompanion(thumbnailAssetId: Value(assetIds.first)));
|
||||||
});
|
});
|
||||||
|
|
||||||
return assetIds.length;
|
return assetIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts a placeholder `remote_asset_entity` row from a freshly-uploaded
|
||||||
|
/// local asset. Skips silently if a row with the same id or
|
||||||
|
/// (owner_id, checksum) already exists — sync will overwrite with the
|
||||||
|
/// authoritative server data once the AssetUploadReadyV1 event is processed.
|
||||||
|
Future<void> upsertRemoteAssetStub({
|
||||||
|
required String remoteId,
|
||||||
|
required String ownerId,
|
||||||
|
required LocalAsset source,
|
||||||
|
}) async {
|
||||||
|
await _db
|
||||||
|
.into(_db.remoteAssetEntity)
|
||||||
|
.insert(
|
||||||
|
RemoteAssetEntityCompanion(
|
||||||
|
id: Value(remoteId),
|
||||||
|
ownerId: Value(ownerId),
|
||||||
|
checksum: Value(source.checksum ?? remoteId),
|
||||||
|
name: Value(source.name),
|
||||||
|
type: Value(source.type),
|
||||||
|
createdAt: Value(source.createdAt),
|
||||||
|
updatedAt: Value(source.updatedAt),
|
||||||
|
width: Value(source.width),
|
||||||
|
height: Value(source.height),
|
||||||
|
durationMs: Value(source.durationMs),
|
||||||
|
isFavorite: Value(source.isFavorite),
|
||||||
|
visibility: const Value(AssetVisibility.timeline),
|
||||||
|
isEdited: Value(source.isEdited),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addUsers(String albumId, List<String> userIds) {
|
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||||
final albumUsers = userIds.map(
|
final albumUsers = userIds.map(
|
||||||
(assetId) => RemoteAlbumUserEntityCompanion(
|
(assetId) => RemoteAlbumUserEntityCompanion(
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> emptyTrash(String ownerId) async {
|
||||||
|
await _db.remoteAssetEntity.deleteWhere((t) => t.deletedAt.isNotNull() & t.ownerId.equals(ownerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restoreAllTrash(String ownerId) async {
|
||||||
|
await (_db.remoteAssetEntity.update()..where((t) => t.deletedAt.isNotNull() & t.ownerId.equals(ownerId))).write(
|
||||||
|
const RemoteAssetEntityCompanion(deletedAt: Value(null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete(List<String> ids) {
|
Future<void> delete(List<String> ids) {
|
||||||
return _db.batch((batch) {
|
return _db.batch((batch) {
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
@@ -38,7 +36,6 @@ class SyncApiRepository {
|
|||||||
|
|
||||||
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
||||||
|
|
||||||
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
|
|
||||||
final request = http.Request('POST', Uri.parse(endpoint));
|
final request = http.Request('POST', Uri.parse(endpoint));
|
||||||
request.headers.addAll(headers);
|
request.headers.addAll(headers);
|
||||||
request.body = jsonEncode(
|
request.body = jsonEncode(
|
||||||
@@ -77,7 +74,6 @@ class SyncApiRepository {
|
|||||||
? SyncRequestType.assetFacesV2
|
? SyncRequestType.assetFacesV2
|
||||||
: SyncRequestType.assetFacesV1,
|
: SyncRequestType.assetFacesV1,
|
||||||
],
|
],
|
||||||
reset: shouldReset,
|
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,9 +97,6 @@ class SyncApiRepository {
|
|||||||
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
|
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset after successful stream start
|
|
||||||
await Store.put(StoreKey.shouldResetSync, false);
|
|
||||||
|
|
||||||
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
||||||
if (shouldAbort) {
|
if (shouldAbort) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.da
|
|||||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||||
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/local_album.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';
|
||||||
@@ -45,25 +46,35 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
// foreign_keys PRAGMA is no-op within transactions
|
// foreign_keys PRAGMA is no-op within transactions
|
||||||
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
|
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
|
||||||
await _db.customStatement('PRAGMA foreign_keys = OFF');
|
await _db.customStatement('PRAGMA foreign_keys = OFF');
|
||||||
await transaction(() async {
|
try {
|
||||||
await _db.assetFaceEntity.deleteAll();
|
await transaction(() async {
|
||||||
await _db.memoryAssetEntity.deleteAll();
|
// FK cascade (ON DELETE SET NULL) does not fire while foreign_keys = OFF,
|
||||||
await _db.memoryEntity.deleteAll();
|
// so null linkedRemoteAlbumId manually to avoid dangling pointers in local_album_entity.
|
||||||
await _db.partnerEntity.deleteAll();
|
await _db.localAlbumEntity.update().write(
|
||||||
await _db.personEntity.deleteAll();
|
const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)),
|
||||||
await _db.remoteAlbumAssetEntity.deleteAll();
|
);
|
||||||
await _db.remoteAlbumEntity.deleteAll();
|
await _db.assetFaceEntity.deleteAll();
|
||||||
await _db.remoteAlbumUserEntity.deleteAll();
|
await _db.memoryAssetEntity.deleteAll();
|
||||||
await _db.remoteAssetEntity.deleteAll();
|
await _db.memoryEntity.deleteAll();
|
||||||
await _db.remoteExifEntity.deleteAll();
|
await _db.partnerEntity.deleteAll();
|
||||||
await _db.stackEntity.deleteAll();
|
await _db.personEntity.deleteAll();
|
||||||
await _db.authUserEntity.deleteAll();
|
await _db.remoteAlbumAssetEntity.deleteAll();
|
||||||
await _db.userEntity.deleteAll();
|
await _db.remoteAlbumEntity.deleteAll();
|
||||||
await _db.userMetadataEntity.deleteAll();
|
await _db.remoteAlbumUserEntity.deleteAll();
|
||||||
await _db.remoteAssetCloudIdEntity.deleteAll();
|
await _db.remoteAssetEntity.deleteAll();
|
||||||
await _db.assetEditEntity.deleteAll();
|
await _db.remoteExifEntity.deleteAll();
|
||||||
});
|
await _db.stackEntity.deleteAll();
|
||||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
await _db.authUserEntity.deleteAll();
|
||||||
|
await _db.userEntity.deleteAll();
|
||||||
|
await _db.userMetadataEntity.deleteAll();
|
||||||
|
await _db.remoteAssetCloudIdEntity.deleteAll();
|
||||||
|
await _db.assetEditEntity.deleteAll();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// re-enable FK even if the transaction throws, otherwise the connection
|
||||||
|
// would be left with foreign_keys = OFF, silently disabling cascades.
|
||||||
|
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncResetV1', error, stack);
|
_logger.severe('Error: SyncResetV1', error, stack);
|
||||||
@@ -191,6 +202,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
type: Value(asset.type.toAssetType()),
|
type: Value(asset.type.toAssetType()),
|
||||||
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
||||||
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
||||||
|
uploadedAt: Value(asset.createdAt),
|
||||||
durationMs: Value(asset.duration?.toDuration()?.inMilliseconds ?? 0),
|
durationMs: Value(asset.duration?.toDuration()?.inMilliseconds ?? 0),
|
||||||
checksum: Value(asset.checksum),
|
checksum: Value(asset.checksum),
|
||||||
isFavorite: Value(asset.isFavorite),
|
isFavorite: Value(asset.isFavorite),
|
||||||
@@ -229,6 +241,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
type: Value(asset.type.toAssetType()),
|
type: Value(asset.type.toAssetType()),
|
||||||
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
||||||
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
||||||
|
uploadedAt: Value(asset.createdAt),
|
||||||
durationMs: Value(asset.duration),
|
durationMs: Value(asset.duration),
|
||||||
checksum: Value(asset.checksum),
|
checksum: Value(asset.checksum),
|
||||||
isFavorite: Value(asset.isFavorite),
|
isFavorite: Value(asset.isFavorite),
|
||||||
|
|||||||
@@ -14,4 +14,13 @@ class TagsApiRepository extends ApiRepository {
|
|||||||
Future<List<TagResponseDto>?> getAllTags() async {
|
Future<List<TagResponseDto>?> getAllTags() async {
|
||||||
return await _api.getAllTags();
|
return await _api.getAllTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||||
|
final response = await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
|
||||||
|
return response?.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TagResponseDto>?> upsertTags(List<String> tags) async {
|
||||||
|
return _api.upsertTags(TagUpsertDto(tags: tags));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
type: row.type,
|
type: row.type,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
|
uploadedAt: row.uploadedAt,
|
||||||
thumbHash: row.thumbHash,
|
thumbHash: row.thumbHash,
|
||||||
width: row.width,
|
width: row.width,
|
||||||
height: row.height,
|
height: row.height,
|
||||||
@@ -317,6 +318,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
origin: TimelineOrigin.remoteAssets,
|
origin: TimelineOrigin.remoteAssets,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TimelineQuery recentlyAdded(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||||
|
filter: (row) =>
|
||||||
|
row.uploadedAt.isNotNull() &
|
||||||
|
row.deletedAt.isNull() &
|
||||||
|
row.ownerId.equals(userId) &
|
||||||
|
(row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
|
||||||
|
origin: TimelineOrigin.recentlyAdded,
|
||||||
|
groupBy: groupBy,
|
||||||
|
sortBy: SortAssetsBy.uploaded,
|
||||||
|
);
|
||||||
|
|
||||||
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||||
filter: (row) =>
|
filter: (row) =>
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
@@ -597,9 +609,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
required TimelineOrigin origin,
|
required TimelineOrigin origin,
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
bool joinLocal = false,
|
bool joinLocal = false,
|
||||||
|
SortAssetsBy sortBy = SortAssetsBy.taken,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
|
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
|
||||||
assetSource: (offset, count) =>
|
assetSource: (offset, count) =>
|
||||||
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
|
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
|
||||||
origin: origin,
|
origin: origin,
|
||||||
@@ -609,6 +622,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
Stream<List<Bucket>> _watchRemoteBucket({
|
Stream<List<Bucket>> _watchRemoteBucket({
|
||||||
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
SortAssetsBy sortBy = SortAssetsBy.taken,
|
||||||
}) {
|
}) {
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
final query = _db.remoteAssetEntity.count(where: filter);
|
final query = _db.remoteAssetEntity.count(where: filter);
|
||||||
@@ -616,7 +630,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy);
|
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy, sortBy: sortBy);
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
@@ -692,8 +706,13 @@ extension on Expression<DateTime> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on $RemoteAssetEntityTable {
|
extension on $RemoteAssetEntityTable {
|
||||||
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy) =>
|
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy, {SortAssetsBy sortBy = SortAssetsBy.taken}) {
|
||||||
coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
|
if (sortBy == SortAssetsBy.uploaded) {
|
||||||
|
return uploadedAt.dateFmt(groupBy, toLocal: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on String {
|
extension on String {
|
||||||
|
|||||||
+21
-8
@@ -179,19 +179,32 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
|
|
||||||
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
|
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
|
||||||
|
|
||||||
|
PageRouteInfo? route;
|
||||||
if (deepLink.uri.scheme == "immich") {
|
if (deepLink.uri.scheme == "immich") {
|
||||||
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart);
|
route = await deepLinkHandler.handleScheme(deepLink, ref);
|
||||||
|
} else if (deepLink.uri.host == "my.immich.app") {
|
||||||
return proposedRoute;
|
route = await deepLinkHandler.handleMyImmichApp(deepLink, ref);
|
||||||
|
} else {
|
||||||
|
return DeepLink.path(deepLink.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deepLink.uri.host == "my.immich.app") {
|
if (route == null) {
|
||||||
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart);
|
return isColdStart ? DeepLink.defaultPath : DeepLink.none;
|
||||||
|
|
||||||
return proposedRoute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeepLink.path(deepLink.path);
|
// We need to replace the route if the destination is the current route
|
||||||
|
if (!isColdStart) {
|
||||||
|
unawaited(
|
||||||
|
ref.read(appRouterProvider).pushAndPopUntil(route, predicate: (r) => r.settings.name != route!.routeName),
|
||||||
|
);
|
||||||
|
return DeepLink.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeepLink([
|
||||||
|
// we need something to segue back to if the app was cold started
|
||||||
|
if (isColdStart) const TabShellRoute(children: [MainTimelineRoute()]),
|
||||||
|
route,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
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' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
|
||||||
class SettingsHeader {
|
class SettingsHeader {
|
||||||
String key = "";
|
String key = "";
|
||||||
@@ -24,17 +22,14 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
final headers = useState<List<SettingsHeader>>([]);
|
final headers = useState<List<SettingsHeader>>([]);
|
||||||
final setInitialHeaders = useState(false);
|
final setInitialHeaders = useState(false);
|
||||||
|
|
||||||
var headersStr = Store.get(StoreKey.customHeaders, "");
|
final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
|
||||||
if (!setInitialHeaders.value) {
|
if (!setInitialHeaders.value) {
|
||||||
if (headersStr.isNotEmpty) {
|
storedHeaders.forEach((k, v) {
|
||||||
var customHeaders = jsonDecode(headersStr) as Map;
|
final header = SettingsHeader();
|
||||||
customHeaders.forEach((k, v) {
|
header.key = k;
|
||||||
final header = SettingsHeader();
|
header.value = v;
|
||||||
header.key = k;
|
headers.value.add(header);
|
||||||
header.value = v;
|
});
|
||||||
headers.value.add(header);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// add first one to help the user
|
// add first one to help the user
|
||||||
if (headers.value.isEmpty) {
|
if (headers.value.isEmpty) {
|
||||||
@@ -88,8 +83,8 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
|
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
|
||||||
final headersMap = {};
|
final headersMap = <String, String>{};
|
||||||
for (var header in headers) {
|
for (final header in headers) {
|
||||||
final key = header.key.trim();
|
final key = header.key.trim();
|
||||||
final value = header.value.trim();
|
final value = header.value.trim();
|
||||||
|
|
||||||
@@ -99,8 +94,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
headersMap[key] = value;
|
headersMap[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
var encoded = jsonEncode(headersMap);
|
await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap);
|
||||||
await Store.put(StoreKey.customHeaders, encoded);
|
|
||||||
await ref.read(apiServiceProvider).updateHeaders();
|
await ref.read(apiServiceProvider).updateHeaders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart';
|
import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart';
|
||||||
@@ -28,71 +27,41 @@ class SharedLinkPage extends HookConsumerWidget {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
Widget buildNoShares() {
|
Widget buildNoShares() {
|
||||||
return Column(
|
return Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Padding(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
children: [
|
||||||
child: const Text(
|
Icon(Icons.link_off, size: 100, color: Theme.of(context).colorScheme.onSurface.withAlpha(128)),
|
||||||
"shared_link_manage_links",
|
const SizedBox(height: 20),
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
|
const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(),
|
||||||
).tr(),
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
child: const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.link_off, size: 100, color: context.themeData.iconTheme.color?.withValues(alpha: 0.5)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSharesList(List<SharedLink> links) {
|
Widget buildSharesList(List<SharedLink> links) {
|
||||||
return Column(
|
return LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, constraints) => constraints.maxWidth > 600
|
||||||
children: [
|
? GridView.builder(
|
||||||
Padding(
|
key: const PageStorageKey('shared-links-grid'),
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0),
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
child: Text(
|
crossAxisCount: 2,
|
||||||
"shared_link_manage_links",
|
mainAxisExtent: 180,
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.textTheme.labelLarge?.color?.withAlpha(200)),
|
crossAxisSpacing: 12,
|
||||||
).tr(),
|
mainAxisSpacing: 12,
|
||||||
),
|
),
|
||||||
Expanded(
|
padding: const EdgeInsets.all(12),
|
||||||
child: LayoutBuilder(
|
itemCount: links.length,
|
||||||
builder: (context, constraints) {
|
itemBuilder: (context, index) => SharedLinkItem(links[index]),
|
||||||
if (constraints.maxWidth > 600) {
|
)
|
||||||
// Two column
|
: ListView.separated(
|
||||||
return GridView.builder(
|
key: const PageStorageKey('shared-links-list'),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
crossAxisCount: 2,
|
itemCount: links.length,
|
||||||
mainAxisExtent: 180,
|
itemBuilder: (context, index) => SharedLinkItem(links[index]),
|
||||||
),
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
itemCount: links.length,
|
),
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return SharedLinkItem(links.elementAt(index));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single column
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: links.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return SharedLinkItem(links.elementAt(index));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/services/shared_link.service.dart';
|
import 'package:immich_mobile/services/shared_link.service.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SharedLinkEditPage extends HookConsumerWidget {
|
class SharedLinkEditPage extends HookConsumerWidget {
|
||||||
|
static const int maxFutureDate = 365 * 2;
|
||||||
|
|
||||||
final SharedLink? existingLink;
|
final SharedLink? existingLink;
|
||||||
final List<String>? assetsList;
|
final List<String>? assetsList;
|
||||||
final String? albumId;
|
final String? albumId;
|
||||||
@@ -23,71 +28,82 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const padding = 20.0;
|
|
||||||
final themeData = context.themeData;
|
final themeData = context.themeData;
|
||||||
final colorScheme = context.colorScheme;
|
final colorScheme = context.colorScheme;
|
||||||
|
final externalDomain = ref.watch(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
final displayServerUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
|
final expiryPresets = <(Duration, String)>[
|
||||||
|
(Duration.zero, context.t.never),
|
||||||
|
(const Duration(minutes: 30), context.t.shared_link_edit_expire_after_option_minutes(count: 30)),
|
||||||
|
(const Duration(hours: 1), context.t.shared_link_edit_expire_after_option_hour),
|
||||||
|
(const Duration(hours: 6), context.t.shared_link_edit_expire_after_option_hours(count: 6)),
|
||||||
|
(const Duration(days: 1), context.t.shared_link_edit_expire_after_option_day),
|
||||||
|
(const Duration(days: 7), context.t.shared_link_edit_expire_after_option_days(count: 7)),
|
||||||
|
(const Duration(days: 30), context.t.shared_link_edit_expire_after_option_days(count: 30)),
|
||||||
|
(const Duration(days: 90), context.t.shared_link_edit_expire_after_option_months(count: 3)),
|
||||||
|
(const Duration(days: 365), context.t.shared_link_edit_expire_after_option_year(count: 1)),
|
||||||
|
];
|
||||||
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
||||||
final descriptionFocusNode = useFocusNode();
|
final descriptionFocusNode = useFocusNode();
|
||||||
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
||||||
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
||||||
final slugFocusNode = useFocusNode();
|
final slugFocusNode = useFocusNode();
|
||||||
|
useListenable(slugController);
|
||||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||||
final editExpiry = useState(false);
|
final expiryAfter = useState<DateTime?>(existingLink?.expiresAt?.toLocal());
|
||||||
final expiryAfter = useState(0);
|
final selectedPresetIndex = useState<int?>(existingLink?.expiresAt == null ? 0 : null);
|
||||||
final newShareLink = useState("");
|
final newShareLink = useState("");
|
||||||
|
|
||||||
|
Widget buildSharedLinkRow({required String leading, required String content}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(leading, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildLinkTitle() {
|
Widget buildLinkTitle() {
|
||||||
if (existingLink != null) {
|
if (existingLink != null) {
|
||||||
if (existingLink!.type == SharedLinkSource.album) {
|
if (existingLink!.type == SharedLinkSource.album) {
|
||||||
return Row(
|
return buildSharedLinkRow(leading: context.t.public_album, content: existingLink!.title);
|
||||||
children: [
|
|
||||||
const Text('public_album', style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
|
||||||
const Text(" | ", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
Text(
|
|
||||||
existingLink!.title,
|
|
||||||
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingLink!.type == SharedLinkSource.individual) {
|
if (existingLink!.type == SharedLinkSource.individual) {
|
||||||
return Row(
|
return buildSharedLinkRow(
|
||||||
children: [
|
leading: context.t.shared_link_individual_shared,
|
||||||
const Text('shared_link_individual_shared', style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
content: existingLink!.description ?? "--",
|
||||||
const Text(" | ", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
existingLink!.description ?? "--",
|
|
||||||
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return const Text("create_link_to_share_description", style: TextStyle(fontWeight: FontWeight.bold)).tr();
|
return Text(context.t.create_link_to_share_description, style: const TextStyle(fontWeight: FontWeight.bold));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildDescriptionField() {
|
Widget buildDescriptionField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
focusNode: descriptionFocusNode,
|
focusNode: descriptionFocusNode,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'description'.tr(),
|
labelText: context.t.description,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'shared_link_edit_description_hint'.tr(),
|
hintText: context.t.shared_link_edit_description_hint,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
||||||
);
|
);
|
||||||
@@ -96,16 +112,14 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildPasswordField() {
|
Widget buildPasswordField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'password'.tr(),
|
labelText: context.t.password,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'shared_link_edit_password_hint'.tr(),
|
hintText: context.t.shared_link_edit_password_hint,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,18 +127,16 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildSlugField() {
|
Widget buildSlugField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: slugController,
|
controller: slugController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
focusNode: slugFocusNode,
|
focusNode: slugFocusNode,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'custom_url'.tr(),
|
labelText: slugController.text.isNotEmpty ? context.t.custom_url : null,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'custom_url'.tr(),
|
hintText: context.t.custom_url,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
prefixText: slugController.text.isNotEmpty ? '/s/' : null,
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
prefixStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => slugFocusNode.unfocus(),
|
onTapOutside: (_) => slugFocusNode.unfocus(),
|
||||||
);
|
);
|
||||||
@@ -133,145 +145,182 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildShowMetaButton() {
|
Widget buildShowMetaButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: showMetadata.value,
|
value: showMetadata.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null,
|
onChanged: (value) => showMetadata.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text("show_metadata", style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(),
|
title: Text(
|
||||||
|
context.t.show_metadata,
|
||||||
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildAllowDownloadButton() {
|
Widget buildAllowDownloadButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: allowDownload.value,
|
value: allowDownload.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null,
|
onChanged: (value) => allowDownload.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
"allow_public_user_to_download",
|
context.t.allow_public_user_to_download,
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildAllowUploadButton() {
|
Widget buildAllowUploadButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: allowUpload.value,
|
value: allowUpload.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null,
|
onChanged: (value) => allowUpload.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
"allow_public_user_to_upload",
|
context.t.allow_public_user_to_upload,
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildEditExpiryButton() {
|
String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(context.locale.toString()).add_Hm().format(dateTime);
|
||||||
return SwitchListTile.adaptive(
|
|
||||||
value: editExpiry.value,
|
DateTime? getExpiresAtFromPreset(Duration preset) => preset == Duration.zero ? null : DateTime.now().add(preset);
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null,
|
|
||||||
activeThumbColor: colorScheme.primary,
|
Future<void> selectDate() async {
|
||||||
dense: true,
|
final today = DateTime.now();
|
||||||
title: Text(
|
final safeInitialDate = expiryAfter.value ?? today.add(const Duration(days: 7));
|
||||||
"change_expiration_time",
|
final initialDate = safeInitialDate.isBefore(today) ? today : safeInitialDate;
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
final selectedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: today,
|
||||||
|
lastDate: today.add(const Duration(days: maxFutureDate)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (selectedDate != null && context.mounted) {
|
||||||
|
final isToday =
|
||||||
|
selectedDate.year == today.year && selectedDate.month == today.month && selectedDate.day == today.day;
|
||||||
|
final initialTime = isToday ? TimeOfDay.fromDateTime(today) : const TimeOfDay(hour: 12, minute: 0);
|
||||||
|
|
||||||
|
final selectedTime = await showTimePicker(context: context, initialTime: initialTime);
|
||||||
|
|
||||||
|
if (selectedTime != null) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
var finalDateTime = DateTime(
|
||||||
|
selectedDate.year,
|
||||||
|
selectedDate.month,
|
||||||
|
selectedDate.day,
|
||||||
|
selectedTime.hour,
|
||||||
|
selectedTime.minute,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalDateTime.isBefore(now) && isToday) {
|
||||||
|
finalDateTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPresetIndex.value = null;
|
||||||
|
expiryAfter.value = finalDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildExpiryAfterButton() {
|
Widget buildExpiryAfterButton() {
|
||||||
return DropdownMenu(
|
return ExpansionTile(
|
||||||
label: Text(
|
title: Text(
|
||||||
"expire_after",
|
context.t.expire_after,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
enableSearch: false,
|
subtitle: Text(
|
||||||
enableFilter: false,
|
expiryAfter.value == null ? context.t.shared_link_expires_never : formatDateTime(expiryAfter.value!),
|
||||||
width: context.width - 40,
|
style: TextStyle(color: themeData.colorScheme.primary),
|
||||||
initialSelection: expiryAfter.value,
|
),
|
||||||
enabled: newShareLink.value.isEmpty && (existingLink == null || editExpiry.value),
|
|
||||||
onSelected: (value) {
|
|
||||||
expiryAfter.value = value!;
|
|
||||||
},
|
|
||||||
dropdownMenuEntries: [
|
|
||||||
DropdownMenuEntry(value: 0, label: "never".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 30,
|
|
||||||
label: "shared_link_edit_expire_after_option_minutes".tr(namedArgs: {'count': "30"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(value: 60, label: "shared_link_edit_expire_after_option_hour".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 6,
|
|
||||||
label: "shared_link_edit_expire_after_option_hours".tr(namedArgs: {'count': "6"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(value: 60 * 24, label: "shared_link_edit_expire_after_option_day".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 7,
|
|
||||||
label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "7"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30,
|
|
||||||
label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "30"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30 * 3,
|
|
||||||
label: "shared_link_edit_expire_after_option_months".tr(namedArgs: {'count': "3"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30 * 12,
|
|
||||||
label: "shared_link_edit_expire_after_option_year".tr(namedArgs: {'count': "1"}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void copyLinkToClipboard() {
|
|
||||||
Clipboard.setData(ClipboardData(text: newShareLink.value)).then((_) {
|
|
||||||
context.scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
"shared_link_clipboard_copied_massage",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
|
||||||
).tr(),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNewLinkField() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
const Padding(padding: EdgeInsets.only(top: 20, bottom: 20), child: Divider()),
|
|
||||||
TextFormField(
|
|
||||||
readOnly: true,
|
|
||||||
initialValue: newShareLink.value,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
|
||||||
suffixIcon: IconButton(onPressed: copyLinkToClipboard, icon: const Icon(Icons.copy)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: Align(
|
child: Column(
|
||||||
alignment: Alignment.bottomRight,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: ElevatedButton(
|
children: [
|
||||||
onPressed: () {
|
Wrap(
|
||||||
context.maybePop();
|
spacing: 8,
|
||||||
},
|
runSpacing: 8,
|
||||||
child: const Text("done", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
|
children: List.generate(expiryPresets.length, (index) {
|
||||||
),
|
final preset = expiryPresets[index];
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(preset.$2),
|
||||||
|
selected: selectedPresetIndex.value == index,
|
||||||
|
onSelected: (_) {
|
||||||
|
selectedPresetIndex.value = index;
|
||||||
|
expiryAfter.value = getExpiresAtFromPreset(preset.$1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
if (expiryAfter.value != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: selectDate,
|
||||||
|
icon: const Icon(Icons.edit_calendar),
|
||||||
|
label: Text(context.t.edit_date_and_time),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime calculateExpiry() {
|
Future<void> copyToClipboard(String link) async {
|
||||||
return DateTime.now().add(Duration(minutes: expiryAfter.value));
|
await Clipboard.setData(ClipboardData(text: link));
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.t.shared_link_clipboard_copied_massage,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildLinkCopyField(String link) {
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => copyToClipboard(link),
|
||||||
|
initialValue: link,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
||||||
|
suffixIcon: IconButton(onPressed: () => Share.share(link), icon: const Icon(Icons.share)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNewLinkReadyScreen() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add_link, size: 100, color: themeData.colorScheme.primary),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
buildLinkCopyField(newShareLink.value),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => context.maybePop(),
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(context.t.done, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? calculateExpiry() => expiryAfter.value;
|
||||||
|
|
||||||
Future<void> handleNewLink() async {
|
Future<void> handleNewLink() async {
|
||||||
final newLink = await ref
|
final newLink = await ref
|
||||||
.read(sharedLinkServiceProvider)
|
.read(sharedLinkServiceProvider)
|
||||||
@@ -284,30 +333,30 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
||||||
password: passwordController.text.isEmpty ? null : passwordController.text,
|
password: passwordController.text.isEmpty ? null : passwordController.text,
|
||||||
slug: slugController.text.isEmpty ? null : slugController.text,
|
slug: slugController.text.isEmpty ? null : slugController.text,
|
||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: calculateExpiry()?.toUtc(),
|
||||||
);
|
);
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
|
||||||
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
|
||||||
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
final serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
|
||||||
serverUrl += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null) {
|
||||||
final hasSlug = newLink.slug?.isNotEmpty == true;
|
newShareLink.value = buildSharedLinkUrl(baseUrl: serverUrl, slug: newLink.slug, key: newLink.key) ?? '';
|
||||||
final urlPath = hasSlug ? newLink.slug : newLink.key;
|
await copyToClipboard(newShareLink.value);
|
||||||
final basePath = hasSlug ? 's' : 'share';
|
} else {
|
||||||
newShareLink.value = "$serverUrl$basePath/$urlPath";
|
|
||||||
copyLinkToClipboard();
|
|
||||||
} else if (newLink == null) {
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
msg: 'shared_link_create_error'.tr(),
|
msg: context.t.shared_link_create_error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,8 +397,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
slug = existingLink!.slug;
|
slug = existingLink!.slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editExpiry.value) {
|
final newExpiry = expiryAfter.value;
|
||||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
if (newExpiry?.toUtc() != existingLink!.expiresAt?.toUtc()) {
|
||||||
|
expiry = newExpiry;
|
||||||
changeExpiry = true;
|
changeExpiry = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,69 +413,115 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
description: desc,
|
description: desc,
|
||||||
password: password,
|
password: password,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry?.toUtc(),
|
||||||
changeExpiry: changeExpiry,
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
await context.maybePop();
|
await context.maybePop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleDeleteLink() async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => ConfirmDialog(
|
||||||
|
title: "delete_shared_link_dialog_title",
|
||||||
|
content: "confirm_delete_shared_link",
|
||||||
|
onOk: () async {
|
||||||
|
await ref.read(sharedLinkServiceProvider).deleteSharedLink(existingLink!.id);
|
||||||
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
await context.maybePop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(existingLink == null ? "create_link_to_share" : "edit_link").tr(),
|
title: Text(existingLink == null ? context.t.create_link_to_share : context.t.edit_link),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: const CloseButton(),
|
leading: const CloseButton(),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListView(
|
child: newShareLink.value.isEmpty
|
||||||
children: [
|
? Padding(
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
child: ListView(
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
children: [
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
|
const SizedBox(height: 20),
|
||||||
Padding(
|
buildLinkTitle(),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
if (existingLink != null)
|
||||||
child: buildShowMetaButton(),
|
Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 16),
|
||||||
child: buildAllowDownloadButton(),
|
buildLinkCopyField(
|
||||||
),
|
buildSharedLinkUrl(
|
||||||
Padding(
|
baseUrl: displayServerUrl,
|
||||||
padding: const EdgeInsets.only(left: padding, right: 20, bottom: 20),
|
slug: existingLink!.slug,
|
||||||
child: buildAllowUploadButton(),
|
key: existingLink!.key,
|
||||||
),
|
) ??
|
||||||
if (existingLink != null)
|
'',
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 24),
|
||||||
child: buildEditExpiryButton(),
|
const Divider(),
|
||||||
),
|
],
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 24),
|
||||||
child: buildExpiryAfterButton(),
|
buildDescriptionField(),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
if (newShareLink.value.isEmpty)
|
buildPasswordField(),
|
||||||
Align(
|
const SizedBox(height: 16),
|
||||||
alignment: Alignment.bottomRight,
|
buildSlugField(),
|
||||||
child: Padding(
|
const SizedBox(height: 16),
|
||||||
padding: const EdgeInsets.only(right: padding + 10, bottom: padding),
|
buildShowMetaButton(),
|
||||||
child: ElevatedButton(
|
const SizedBox(height: 16),
|
||||||
onPressed: existingLink != null ? handleEditLink : handleNewLink,
|
buildAllowDownloadButton(),
|
||||||
child: Text(
|
const SizedBox(height: 16),
|
||||||
existingLink != null ? "shared_link_edit_submit_button" : "create_link",
|
buildAllowUploadButton(),
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
const SizedBox(height: 16),
|
||||||
).tr(),
|
buildExpiryAfterButton(),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
if (existingLink != null)
|
||||||
|
OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: themeData.colorScheme.error,
|
||||||
|
side: BorderSide(color: themeData.colorScheme.error),
|
||||||
|
),
|
||||||
|
onPressed: handleDeleteLink,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: Text(
|
||||||
|
context.t.delete,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
onPressed: existingLink != null ? handleEditLink : handleNewLink,
|
||||||
|
label: Text(
|
||||||
|
existingLink != null ? context.t.shared_link_edit_submit_button : context.t.create_link,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
if (newShareLink.value.isNotEmpty)
|
: Center(child: buildNewLinkReadyScreen()),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
|
||||||
child: buildNewLinkField(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -277,7 +277,7 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onAndroidUpload();
|
Future<void> onAndroidUpload(int? maxMinutes);
|
||||||
|
|
||||||
Future<void> cancel();
|
Future<void> cancel();
|
||||||
|
|
||||||
@@ -323,8 +323,10 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
pigeonVar_channel.setMessageHandler(null);
|
pigeonVar_channel.setMessageHandler(null);
|
||||||
} else {
|
} else {
|
||||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||||
|
final List<Object?> args = message! as List<Object?>;
|
||||||
|
final int? arg_maxMinutes = args[0] as int?;
|
||||||
try {
|
try {
|
||||||
await api.onAndroidUpload();
|
await api.onAndroidUpload(arg_maxMinutes);
|
||||||
return wrapResponse(empty: true);
|
return wrapResponse(empty: true);
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
return wrapResponse(error: e);
|
return wrapResponse(error: e);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
|
|
||||||
final scrollView = CustomScrollView(
|
final scrollView = CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
ImmichSliverAppBar(
|
ImmichSliverAppBar(
|
||||||
snap: false,
|
snap: false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.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/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
||||||
@@ -22,17 +21,13 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? [];
|
||||||
if (user == null) {
|
final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers);
|
||||||
throw Exception('User must be logged in to access asset selection timeline');
|
|
||||||
}
|
|
||||||
|
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: const Timeline(showStorageIndicator: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,17 +179,14 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final album = await ref
|
final album = await ref
|
||||||
.watch(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.createAlbum(
|
.createAlbumWithAssets(
|
||||||
title: title,
|
title: title,
|
||||||
description: albumDescriptionController.text.trim(),
|
description: albumDescriptionController.text.trim(),
|
||||||
assetIds: selectedAssets.map((asset) {
|
assets: selectedAssets,
|
||||||
final remoteAsset = asset as RemoteAsset;
|
|
||||||
return remoteAsset.id;
|
|
||||||
}).toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (album != null) {
|
if (album != null && context.mounted) {
|
||||||
unawaited(context.replaceRoute(RemoteAlbumRoute(album: album)));
|
unawaited(context.replaceRoute(RemoteAlbumRoute(album: album)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftRecentlyAddedPage extends StatelessWidget {
|
||||||
|
const DriftRecentlyAddedPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access recently taken');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService = ref.watch(timelineFactoryProvider).recentlyAdded(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_added'.t())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/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/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/pending_uploads_banner.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
@@ -39,7 +40,8 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAssets(BuildContext context) async {
|
Future<void> addAssets(BuildContext context) async {
|
||||||
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(_album.id);
|
final notifier = ref.read(remoteAlbumProvider.notifier);
|
||||||
|
final albumAssets = await notifier.getAssets(_album.id);
|
||||||
|
|
||||||
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
||||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
||||||
@@ -49,17 +51,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final added = await ref
|
final added = await notifier.addAssetsToAlbum(_album.id, newAssets);
|
||||||
.read(remoteAlbumProvider.notifier)
|
|
||||||
.addAssets(
|
|
||||||
_album.id,
|
|
||||||
newAssets.map((asset) {
|
|
||||||
final remoteAsset = asset as RemoteAsset;
|
|
||||||
return remoteAsset.id;
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (added > 0) {
|
if (added > 0 && context.mounted) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}),
|
msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}),
|
||||||
@@ -186,6 +180,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
currentRemoteAlbumScopedProvider.overrideWithValue(_album),
|
currentRemoteAlbumScopedProvider.overrideWithValue(_album),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
|
topSliverWidget: PendingUploadsBanner(albumId: _album.id),
|
||||||
appBar: RemoteAlbumSliverAppBar(
|
appBar: RemoteAlbumSliverAppBar(
|
||||||
icon: Icons.photo_album_outlined,
|
icon: Icons.photo_album_outlined,
|
||||||
kebabMenu: _AlbumKebabMenu(
|
kebabMenu: _AlbumKebabMenu(
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
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/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftSlideshowPage extends ConsumerStatefulWidget {
|
||||||
|
final TimelineService timeline;
|
||||||
|
|
||||||
|
const DriftSlideshowPage({super.key, required this.timeline});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftSlideshowPage> createState() => _DriftSlideshowPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> {
|
||||||
|
late SlideshowConfig _config;
|
||||||
|
late final PageController _pageController;
|
||||||
|
late final Stopwatch _stopwatch;
|
||||||
|
late Timer _timer;
|
||||||
|
late int _index;
|
||||||
|
late int _nextIndex;
|
||||||
|
bool _paused = false;
|
||||||
|
bool _showAppBar = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_config = ref.read(appConfigProvider.select((s) => s.slideshow));
|
||||||
|
final asset = ref.read(assetViewerProvider).currentAsset;
|
||||||
|
_index = asset == null ? 0 : widget.timeline.getIndex(asset.heroTag) ?? 0;
|
||||||
|
_pageController = PageController(initialPage: _index);
|
||||||
|
_stopwatch = Stopwatch();
|
||||||
|
_createTimer();
|
||||||
|
_updateNextIndex();
|
||||||
|
ref.listenManual(appConfigProvider.select((s) => s.slideshow), _onConfigChanged);
|
||||||
|
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
unawaited(WakelockPlus.enable());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_pageController.dispose();
|
||||||
|
unawaited(WakelockPlus.disable());
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _play() {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
} else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).play();
|
||||||
|
} else {
|
||||||
|
_nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onConfigChanged(SlideshowConfig? previous, SlideshowConfig next) {
|
||||||
|
if (_config == next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final durationChanged = _config.duration != next.duration;
|
||||||
|
_config = next;
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index);
|
||||||
|
if (durationChanged && !_paused && asset?.isImage == true) {
|
||||||
|
_timer.cancel();
|
||||||
|
_createTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateNextIndex() {
|
||||||
|
_nextIndex = switch (_config.direction) {
|
||||||
|
SlideshowDirection.forward => _index + 1,
|
||||||
|
SlideshowDirection.backward => _index - 1,
|
||||||
|
SlideshowDirection.shuffle => widget.timeline.getIndex(widget.timeline.getRandomAsset().heroTag)!,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextPage() async {
|
||||||
|
if (_nextIndex < 0 || _nextIndex >= widget.timeline.totalAssets) {
|
||||||
|
if (_config.repeat) {
|
||||||
|
final wrapped = _config.direction == SlideshowDirection.forward ? 0 : widget.timeline.totalAssets - 1;
|
||||||
|
await widget.timeline.preloadAssets(wrapped);
|
||||||
|
_pageController.jumpToPage(wrapped);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_paused = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
await widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.direction == SlideshowDirection.shuffle || !_config.transition) {
|
||||||
|
_pageController.jumpToPage(_nextIndex);
|
||||||
|
} else {
|
||||||
|
unawaited(_pageController.animateToPage(_nextIndex, duration: Durations.long2, curve: Curves.easeIn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createTimer() {
|
||||||
|
_timer = Timer(Duration(milliseconds: _config.duration * 1000 - _stopwatch.elapsedMilliseconds), () {
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
_nextPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
_stopwatch.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pageChanged(int page) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(page)!;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_index = page;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
_paused = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
|
||||||
|
if (!_paused && asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp() async {
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(_showAppBar ? SystemUiMode.immersive : SystemUiMode.edgeToEdge);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_showAppBar = !_showAppBar;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getProgressBar(BuildContext context) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
key: Key(_index.toString()),
|
||||||
|
tween: Tween<double>(begin: elapsed / duration.toDouble(), end: _paused ? elapsed / duration.toDouble() : 1.0),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value:
|
||||||
|
ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds /
|
||||||
|
asset.duration.inMilliseconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getBlur(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: getFullImageProvider(asset, size: Size(context.width, context.height)),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(color: Colors.black.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getPhotoView(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return const Center(child: ImmichLoadingIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final scale = _config.look == SlideshowLook.cover
|
||||||
|
? PhotoViewComputedScale.covered
|
||||||
|
: PhotoViewComputedScale.contained;
|
||||||
|
final isCurrent = _index == index;
|
||||||
|
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final zoomOut = index % 2 == 1;
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
final progress = zoomOut ? 1.0 - elapsed / duration.toDouble() : elapsed / duration.toDouble();
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: progress,
|
||||||
|
end: _paused
|
||||||
|
? progress
|
||||||
|
: zoomOut
|
||||||
|
? 0.0
|
||||||
|
: 1.0,
|
||||||
|
),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => PhotoView(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
index: index,
|
||||||
|
disableScaleGestures: true,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale * (1.0 + value / 10.0),
|
||||||
|
controller: PhotoViewController(),
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||||
|
final position = ref.read(videoPlayerProvider(asset.heroTag)).position;
|
||||||
|
|
||||||
|
if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) {
|
||||||
|
_nextPage();
|
||||||
|
} else if (status == VideoPlaybackStatus.playing) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhotoView.customChild(
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
disableScaleGestures: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale,
|
||||||
|
child: NativeVideoViewer(
|
||||||
|
asset: asset,
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: PreferredSize(
|
||||||
|
preferredSize: Size(AppBar().preferredSize.width, AppBar().preferredSize.height + 5),
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !_showAppBar,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _showAppBar ? 1.0 : 0.0,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
title: Text("slideshow".t(context: context)),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _paused ? _play : _pause,
|
||||||
|
icon: Icon(_paused ? Icons.play_arrow : Icons.pause),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_pause();
|
||||||
|
context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer));
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_getProgressBar(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
extendBody: true,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: PhotoViewGestureDetectorScope(
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const FastClampingScrollPhysics(),
|
||||||
|
itemCount: widget.timeline.totalAssets,
|
||||||
|
onPageChanged: _pageChanged,
|
||||||
|
itemBuilder: (context, index) => Stack(
|
||||||
|
children: [
|
||||||
|
if (_config.look == SlideshowLook.blurredBackground) _getBlur(context, index),
|
||||||
|
_getPhotoView(context, index),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user