Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3946e8c0b2 | |||
| 1dbac30993 | |||
| 831fb5a2f9 | |||
| 8d272f8abc | |||
| 3a2b572e0b | |||
| 942b27241a | |||
| 482526475b | |||
| c4bd24277a |
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "github",
|
"scripts": {
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write ."
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.5.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,3 @@ The `/api/something` endpoint is now `/api/something-else`
|
|||||||
- [ ] I have followed naming conventions/patterns in the surrounding code
|
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||||
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
||||||
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
||||||
|
|
||||||
## Please describe to which degree, if any, an LLM was used in creating this pull request.
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|||||||
+15
-11
@@ -33,20 +33,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './cli/.nvmrc'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install && pnpm run build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install dependencies
|
- run: pnpm install --frozen-lockfile
|
||||||
run: mise run cli:install
|
- run: pnpm build
|
||||||
|
- run: pnpm publish
|
||||||
- name: Run build
|
|
||||||
run: mise run cli:build
|
|
||||||
|
|
||||||
- name: Publish package
|
|
||||||
run: pnpm publish
|
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -55,17 +55,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './docs/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run install
|
- name: Run install
|
||||||
run: mise run docs:install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: mise run docs:format-fix
|
run: pnpm format
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: mise run docs:build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Upload build output
|
- name: Upload build output
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ jobs:
|
|||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Setup mise
|
- name: Setup Node
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Fix formatting
|
- name: Fix formatting
|
||||||
run: mise run server:format-fix && mise run web:format-fix && mise run docs:format-fix
|
run: make install-all && make format-all
|
||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
||||||
|
|||||||
@@ -46,8 +46,15 @@ jobs:
|
|||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
|
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -20,15 +20,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
|
# Setup .npmrc file to publish to npm
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: mise run sdk:install
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mise run sdk:build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: pnpm publish
|
run: pnpm publish
|
||||||
env:
|
env:
|
||||||
|
|||||||
+184
-86
@@ -72,21 +72,27 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run package manager install
|
- name: Run package manager install
|
||||||
run: mise run server:install
|
run: pnpm install
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: mise run server:lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: mise run server:format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: mise run server:check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run small tests & coverage
|
- name: Run small tests & coverage
|
||||||
run: mise run server:test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Unit Test CLI
|
name: Unit Test CLI
|
||||||
@@ -103,23 +109,30 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './cli/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install && pnpm run build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: mise run cli:install
|
run: pnpm install
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: mise run cli:lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: mise run cli:format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: mise run cli:check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: mise run cli:test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
cli-unit-tests-win:
|
cli-unit-tests-win:
|
||||||
name: Unit Test CLI (Windows)
|
name: Unit Test CLI (Windows)
|
||||||
@@ -136,18 +149,25 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './cli/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: mise run cli:install
|
run: pnpm install --frozen-lockfile
|
||||||
# Skip linter & formatter in Windows test.
|
# Skip linter & formatter in Windows test.
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: mise run cli:check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: mise run cli:test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
web-lint:
|
web-lint:
|
||||||
name: Lint Web
|
name: Lint Web
|
||||||
@@ -156,25 +176,35 @@ jobs:
|
|||||||
runs-on: mich
|
runs-on: mich
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './web/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
- name: Run install
|
working-directory: ./open-api/typescript-sdk
|
||||||
run: mise run web:install
|
- name: Run pnpm install
|
||||||
|
run: pnpm rebuild && pnpm install --frozen-lockfile
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: mise run web:lint-p
|
run: pnpm lint:p
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: mise run web:format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run svelte checks
|
- name: Run svelte checks
|
||||||
run: mise run web:check-svelte
|
run: pnpm check:svelte
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Test Web
|
name: Test Web
|
||||||
@@ -183,22 +213,32 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './web/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: mise run web:install
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: mise run web:check
|
run: pnpm check:typescript
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: mise run web:test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
i18n-tests:
|
i18n-tests:
|
||||||
name: Test i18n
|
name: Test i18n
|
||||||
@@ -212,12 +252,18 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './web/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: mise run web:install
|
run: pnpm --filter=immich-web install --frozen-lockfile
|
||||||
- name: Format
|
- name: Format
|
||||||
run: mise run i18n:format-fix
|
run: pnpm --filter=immich-web format:i18n
|
||||||
- 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
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
@@ -247,22 +293,29 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: mise run e2e:install
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: mise run e2e:lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: mise run e2e:format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: mise run e2e:check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
server-medium-tests:
|
server-medium-tests:
|
||||||
name: Medium Tests (Server)
|
name: Medium Tests (Server)
|
||||||
@@ -271,17 +324,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
- name: Run install
|
- name: Setup Node
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true mise run server:install
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- name: Run pnpm install
|
||||||
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
run: mise run server:test-medium
|
run: pnpm test:medium
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
e2e-tests-server-cli:
|
e2e-tests-server-cli:
|
||||||
name: End-to-End Tests (Server & CLI)
|
name: End-to-End Tests (Server & CLI)
|
||||||
@@ -290,6 +352,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./e2e
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
@@ -299,25 +364,34 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run setup web
|
- name: Run setup web
|
||||||
run: mise run web:install && mise run web:svelte-kit-sync
|
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
||||||
|
working-directory: ./web
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: mise run cli:install && mise run cli:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./cli
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: mise run e2e:install
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose -f e2e/docker-compose.yml build
|
run: docker compose build
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: mise run e2e:test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
e2e-tests-web:
|
e2e-tests-web:
|
||||||
name: End-to-End Tests (Web)
|
name: End-to-End Tests (Web)
|
||||||
@@ -326,6 +400,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./e2e
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
@@ -335,26 +412,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: mise run sdk:install && mise run sdk:build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run setup web
|
- name: Install dependencies
|
||||||
run: mise run web:install && mise run web:svelte-kit-sync
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Run setup cli
|
|
||||||
run: mise run cli:install && mise run cli:build
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install chromium --only-shell
|
run: npx playwright install chromium --only-shell
|
||||||
working-directory: e2e
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose -f e2e/docker-compose.yml build
|
run: docker compose build
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
run: mise run e2e:test-web
|
run: npx playwright test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
success-check-e2e:
|
success-check-e2e:
|
||||||
name: End-to-End Tests Success
|
name: End-to-End Tests Success
|
||||||
@@ -439,12 +519,18 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
- name: Run install
|
- name: Setup Node
|
||||||
run: mise run github:install
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './.github/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- name: Run pnpm install
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: mise run github:format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
shellcheck:
|
shellcheck:
|
||||||
name: ShellCheck
|
name: ShellCheck
|
||||||
@@ -470,12 +556,18 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true mise run server:install
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: mise run server:build
|
run: pnpm --filter immich build
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: ./bin/generate-open-api.sh
|
run: ./bin/generate-open-api.sh
|
||||||
working-directory: open-api
|
working-directory: open-api
|
||||||
@@ -519,19 +611,25 @@ jobs:
|
|||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup mise
|
- name: Setup pnpm
|
||||||
uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- 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 the app
|
- name: Build the app
|
||||||
run: mise run server:build
|
run: pnpm build
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: mise run server:migrations run
|
run: pnpm migrations:run
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: mise run server:schema-reset
|
run: pnpm schema:reset
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: mise run server:migrations generate src/TestMigration
|
run: pnpm 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
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
@@ -548,7 +646,7 @@ jobs:
|
|||||||
cat ./src/*-TestMigration.ts
|
cat ./src/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: mise run server:sql
|
run: pnpm sync:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ dev-update: prepare-volumes
|
|||||||
dev-scale: prepare-volumes
|
dev-scale: prepare-volumes
|
||||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
dev-docs:
|
dev-docs: prepare-volumes
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e: prepare-volumes
|
||||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-update:
|
e2e-update: prepare-volumes
|
||||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
e2e-down:
|
e2e-down:
|
||||||
@@ -73,8 +73,6 @@ define safe_chown
|
|||||||
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
|
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
|
||||||
true; \
|
true; \
|
||||||
else \
|
else \
|
||||||
STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \
|
|
||||||
echo "$$STATUS $(1)"; \
|
|
||||||
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
|
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
@@ -85,13 +83,11 @@ prepare-volumes:
|
|||||||
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
|
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
|
||||||
ifneq ($(UPLOAD_LOCATION),)
|
ifneq ($(UPLOAD_LOCATION),)
|
||||||
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
|
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
|
||||||
@mkdir -p "docker/$(UPLOAD_LOCATION)/photos/upload"
|
@mkdir -p "docker/$(UPLOAD_LOCATION)"
|
||||||
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
|
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
|
||||||
@$(call safe_chown,docker/$(UPLOAD_LOCATION)/photos,-R)
|
|
||||||
else
|
else
|
||||||
@mkdir -p "$(UPLOAD_LOCATION)/photos/upload"
|
@mkdir -p "$(UPLOAD_LOCATION)"
|
||||||
@$(call safe_chown,$(UPLOAD_LOCATION),)
|
@$(call safe_chown,$(UPLOAD_LOCATION),)
|
||||||
@$(call safe_chown,$(UPLOAD_LOCATION)/photos,-R)
|
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
@@ -42,6 +42,17 @@
|
|||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"prepack": "npm run build",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:cov": "vitest --coverage",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"check": "tsc --noEmit"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/immich-app/immich.git",
|
"url": "git+https://github.com/immich-app/immich.git",
|
||||||
@@ -56,5 +67,8 @@
|
|||||||
"fastq": "^1.17.1",
|
"fastq": "^1.17.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
@@ -5,7 +5,7 @@ After making any changes in the `server/src/schema`, a database migration need t
|
|||||||
1. Run the command
|
1. Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise run server:migrations generate <migration-name>
|
pnpm run migrations:generate <migration-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
2. Check if the migration file makes sense.
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ When contributing code through a pull request, please check the following:
|
|||||||
|
|
||||||
## Web Checks
|
## Web Checks
|
||||||
|
|
||||||
- [ ] `mise run web:lint` (linting via ESLint)
|
- [ ] `pnpm run lint` (linting via ESLint)
|
||||||
- [ ] `mise run web:format` (formatting via Prettier)
|
- [ ] `pnpm run format` (formatting via Prettier)
|
||||||
- [ ] `mise run web:check` (check typescript)
|
- [ ] `pnpm run check:svelte` (Type checking via SvelteKit)
|
||||||
- [ ] `mise run web:check-svelte` (Type checking via SvelteKit)
|
- [ ] `pnpm run check:typescript` (check typescript)
|
||||||
- [ ] `mise run web:test` (unit tests)
|
- [ ] `pnpm test` (unit tests)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -25,18 +25,17 @@ Run all web checks with `pnpm run check:all`
|
|||||||
|
|
||||||
## Server Checks
|
## Server Checks
|
||||||
|
|
||||||
- [ ] `mise run server:lint` (linting via ESLint)
|
- [ ] `pnpm run lint` (linting via ESLint)
|
||||||
- [ ] `mise run server:format` (formatting via Prettier)
|
- [ ] `pnpm run format` (formatting via Prettier)
|
||||||
- [ ] `mise run server:check` (type checking via `tsc`)
|
- [ ] `pnpm run check` (Type checking via `tsc`)
|
||||||
- [ ] `mise run server:test` (unit tests)
|
- [ ] `pnpm test` (unit tests)
|
||||||
- [ ] `mise run server:test-medium` (medium tests)
|
|
||||||
|
|
||||||
:::tip AIO
|
:::tip AIO
|
||||||
Run all server checks with `pnpm run check:all`
|
Run all server checks with `pnpm run check:all`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info Auto Fix
|
:::info Auto Fix
|
||||||
You can use `mise run server:lint-fix` and `mise run server:format-fix` to potentially correct some issues automatically.
|
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Mobile Checks
|
## Mobile Checks
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
"name": "documentation",
|
"name": "documentation",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"start": "docusaurus start --port 3005",
|
||||||
|
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||||
|
"build": "npm run copy:openapi && docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "~3.8.0",
|
"@docusaurus/core": "~3.8.0",
|
||||||
"@docusaurus/preset-classic": "~3.8.0",
|
"@docusaurus/preset-classic": "~3.8.0",
|
||||||
@@ -44,5 +58,8 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
@@ -4,6 +4,17 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:web": "npx playwright test",
|
||||||
|
"start:web": "npx playwright test --ui",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"check": "tsc --noEmit"
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
@@ -41,5 +52,8 @@
|
|||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"utimes": "^5.2.1",
|
"utimes": "^5.2.1",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1557,7 +1557,6 @@
|
|||||||
"purchase_server_description_2": "Supporter status",
|
"purchase_server_description_2": "Supporter status",
|
||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
"query_asset_id": "Query Asset ID",
|
|
||||||
"queue_status": "Queuing {count}/{total}",
|
"queue_status": "Queuing {count}/{total}",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
@@ -1736,7 +1735,7 @@
|
|||||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"selected_count": "{count, plural, other {# selected}}",
|
"selected_count": "{count, plural, other {# selected}}",
|
||||||
"selected_gps_coordinates": "Selected GPS Coordinates",
|
"selected_gps_coordinates": "selected gps coordinates",
|
||||||
"send_message": "Send message",
|
"send_message": "Send message",
|
||||||
"send_welcome_email": "Send welcome email",
|
"send_welcome_email": "Send welcome email",
|
||||||
"server_endpoint": "Server Endpoint",
|
"server_endpoint": "Server Endpoint",
|
||||||
@@ -2078,7 +2077,6 @@
|
|||||||
"view_next_asset": "View next asset",
|
"view_next_asset": "View next asset",
|
||||||
"view_previous_asset": "View previous asset",
|
"view_previous_asset": "View previous asset",
|
||||||
"view_qr_code": "View QR code",
|
"view_qr_code": "View QR code",
|
||||||
"view_similar_photos": "View similar photos",
|
|
||||||
"view_stack": "View Stack",
|
"view_stack": "View Stack",
|
||||||
"view_user": "View User",
|
"view_user": "View User",
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
[tools.dart]
|
|
||||||
version = "3.8.2"
|
|
||||||
backend = "asdf:dart"
|
|
||||||
|
|
||||||
[tools.flutter]
|
|
||||||
version = "3.32.8-stable"
|
|
||||||
backend = "asdf:flutter"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
|
||||||
version = "1.31.4"
|
|
||||||
backend = "github:CQLabs/homebrew-dcm"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
|
|
||||||
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
|
|
||||||
size = 8821083
|
|
||||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
|
|
||||||
|
|
||||||
[tools.node]
|
|
||||||
version = "22.18.0"
|
|
||||||
backend = "core:node"
|
|
||||||
|
|
||||||
[tools.node.platforms.linux-x64]
|
|
||||||
checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a"
|
|
||||||
size = 54824343
|
|
||||||
url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz"
|
|
||||||
|
|
||||||
[tools.pnpm]
|
|
||||||
version = "10.14.0"
|
|
||||||
backend = "aqua:pnpm/pnpm"
|
|
||||||
|
|
||||||
[tools.pnpm.platforms.linux-x64]
|
|
||||||
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
|
|
||||||
size = 66231525
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
[tools]
|
|
||||||
node = "22.18.0"
|
|
||||||
flutter = "3.32.8"
|
|
||||||
pnpm = "10.14.0"
|
|
||||||
dart = "3.8.2"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
|
||||||
version = "1.31.4"
|
|
||||||
bin = "dcm"
|
|
||||||
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
|
||||||
|
|
||||||
[settings]
|
|
||||||
experimental = true
|
|
||||||
lockfile = true
|
|
||||||
pin = true
|
|
||||||
|
|
||||||
# .github
|
|
||||||
[tasks."github:install"]
|
|
||||||
run = "pnpm install --filter github --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."github:format"]
|
|
||||||
env._.path = "./.github/node_modules/.bin"
|
|
||||||
dir = ".github"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."github:format-fix"]
|
|
||||||
env._.path = "./.github/node_modules/.bin"
|
|
||||||
dir = ".github"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
# @immich/cli
|
|
||||||
[tasks."cli:install"]
|
|
||||||
run = "pnpm install --filter @immich/cli --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."cli:build"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "vite build"
|
|
||||||
|
|
||||||
[tasks."cli:test"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "vite"
|
|
||||||
|
|
||||||
[tasks."cli:lint"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
|
||||||
|
|
||||||
[tasks."cli:lint-fix"]
|
|
||||||
run = "mise run cli:lint --fix"
|
|
||||||
|
|
||||||
[tasks."cli:format"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."cli:format-fix"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
[tasks."cli:check"]
|
|
||||||
env._.path = "./cli/node_modules/.bin"
|
|
||||||
dir = "cli"
|
|
||||||
run = "tsc --noEmit"
|
|
||||||
|
|
||||||
# @immich/sdk
|
|
||||||
[tasks."sdk:install"]
|
|
||||||
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."sdk:build"]
|
|
||||||
env._.path = "./open-api/typescript-sdk/node_modules/.bin"
|
|
||||||
dir = "./open-api/typescript-sdk"
|
|
||||||
run = "tsc"
|
|
||||||
|
|
||||||
# docs
|
|
||||||
[tasks."docs:install"]
|
|
||||||
run = "pnpm install --filter documentation --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."docs:start"]
|
|
||||||
env._.path = "./docs/node_modules/.bin"
|
|
||||||
dir = "docs"
|
|
||||||
run = "docusaurus --port 3005"
|
|
||||||
|
|
||||||
[tasks."docs:build"]
|
|
||||||
env._.path = "./docs/node_modules/.bin"
|
|
||||||
dir = "docs"
|
|
||||||
run = [
|
|
||||||
"jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
|
||||||
"docusaurus build",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[tasks."docs:preview"]
|
|
||||||
env._.path = "./docs/node_modules/.bin"
|
|
||||||
dir = "docs"
|
|
||||||
run = "docusaurus serve"
|
|
||||||
|
|
||||||
|
|
||||||
[tasks."docs:format"]
|
|
||||||
env._.path = "./docs/node_modules/.bin"
|
|
||||||
dir = "docs"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."docs:format-fix"]
|
|
||||||
env._.path = "./docs/node_modules/.bin"
|
|
||||||
dir = "docs"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
|
|
||||||
# e2e
|
|
||||||
[tasks."e2e:install"]
|
|
||||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."e2e:test"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "vitest --run"
|
|
||||||
|
|
||||||
[tasks."e2e:test-web"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "playwright test"
|
|
||||||
|
|
||||||
[tasks."e2e:format"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."e2e:format-fix"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
[tasks."e2e:lint"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
|
||||||
|
|
||||||
[tasks."e2e:lint-fix"]
|
|
||||||
run = "mise run e2e:lint --fix"
|
|
||||||
|
|
||||||
[tasks."e2e:check"]
|
|
||||||
env._.path = "./e2e/node_modules/.bin"
|
|
||||||
dir = "e2e"
|
|
||||||
run = "tsc --noEmit"
|
|
||||||
|
|
||||||
# i18n
|
|
||||||
[tasks."i18n:format"]
|
|
||||||
run = "mise run i18n:format-fix"
|
|
||||||
|
|
||||||
[tasks."i18n:format-fix"]
|
|
||||||
run = "pnpm dlx sort-json ./i18n/*.json"
|
|
||||||
|
|
||||||
|
|
||||||
# server
|
|
||||||
[tasks."server:install"]
|
|
||||||
run = "pnpm install --filter immich --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."server:build"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "nest build"
|
|
||||||
|
|
||||||
[tasks."server:test"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "vitest --config test/vitest.config.mjs"
|
|
||||||
|
|
||||||
[tasks."server:test-medium"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "vitest --config test/vitest.config.medium.mjs"
|
|
||||||
|
|
||||||
[tasks."server:format"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."server:format-fix"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
[tasks."server:lint"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0"
|
|
||||||
|
|
||||||
[tasks."server:lint-fix"]
|
|
||||||
run = "mise run server:lint --fix"
|
|
||||||
|
|
||||||
[tasks."server:check"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "tsc --noEmit"
|
|
||||||
|
|
||||||
[tasks."server:sql"]
|
|
||||||
dir = "server"
|
|
||||||
run = "node ./dist/bin/sync-open-api.js"
|
|
||||||
|
|
||||||
[tasks."server:open-api"]
|
|
||||||
dir = "server"
|
|
||||||
run = "node ./dist/bin/sync-open-api.js"
|
|
||||||
|
|
||||||
[tasks."server:migrations"]
|
|
||||||
dir = "server"
|
|
||||||
run = "node ./dist/bin/migrations.js"
|
|
||||||
description = "Run database migration commands (create, generate, run, debug, or query)"
|
|
||||||
|
|
||||||
[tasks."server:schema-drop"]
|
|
||||||
run = "mise run server:migrations query 'DROP schema public cascade; CREATE schema public;'"
|
|
||||||
|
|
||||||
[tasks."server:schema-reset"]
|
|
||||||
run = "mise run server:schema-drop && mise run server:migrations run"
|
|
||||||
|
|
||||||
[tasks."server:email-dev"]
|
|
||||||
env._.path = "./server/node_modules/.bin"
|
|
||||||
dir = "server"
|
|
||||||
run = "email dev -p 3050 --dir src/emails"
|
|
||||||
|
|
||||||
[tasks."server:checklist"]
|
|
||||||
run = [
|
|
||||||
"mise run server:install",
|
|
||||||
"mise run server:format",
|
|
||||||
"mise run server:lint",
|
|
||||||
"mise run server:check",
|
|
||||||
"mise run server:test-medium --run",
|
|
||||||
"mise run server:test --run",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# web
|
|
||||||
[tasks."web:install"]
|
|
||||||
run = "pnpm install --filter immich-web --frozen-lockfile"
|
|
||||||
|
|
||||||
[tasks."web:svelte-kit-sync"]
|
|
||||||
env._.path = "./web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "svelte-kit sync"
|
|
||||||
|
|
||||||
[tasks."web:build"]
|
|
||||||
env._.path = "./web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "vite build"
|
|
||||||
|
|
||||||
[tasks."web:build-stats"]
|
|
||||||
env.BUILD_STATS = "true"
|
|
||||||
env._.path = "./web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "vite build"
|
|
||||||
|
|
||||||
[tasks."web:preview"]
|
|
||||||
env._.path = "./web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "vite preview"
|
|
||||||
|
|
||||||
[tasks."web:start"]
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "vite dev --host 0.0.0.0 --port 3000"
|
|
||||||
|
|
||||||
[tasks."web:test"]
|
|
||||||
depends = "web:svelte-kit-sync"
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "vitest"
|
|
||||||
|
|
||||||
[tasks."web:format"]
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "prettier --check ."
|
|
||||||
|
|
||||||
[tasks."web:format-fix"]
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "prettier --write ."
|
|
||||||
|
|
||||||
[tasks."web:lint"]
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "eslint . --max-warnings 0"
|
|
||||||
|
|
||||||
[tasks."web:lint-p"]
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "eslint-p . --max-warnings 0 --concurrency=4"
|
|
||||||
|
|
||||||
[tasks."web:lint-fix"]
|
|
||||||
run = "mise run web:lint --fix"
|
|
||||||
|
|
||||||
[tasks."web:check"]
|
|
||||||
depends = "web:svelte-kit-sync"
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "tsc --noEmit"
|
|
||||||
|
|
||||||
[tasks."web:check-svelte"]
|
|
||||||
depends = "web:svelte-kit-sync"
|
|
||||||
env._.path = "web/node_modules/.bin"
|
|
||||||
dir = "web"
|
|
||||||
run = "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte"
|
|
||||||
|
|
||||||
[tasks."web:checklist"]
|
|
||||||
run = [
|
|
||||||
"mise run web:install",
|
|
||||||
"mise run web:format",
|
|
||||||
"mise run web:check",
|
|
||||||
"mise run web:test --run",
|
|
||||||
"mise run web:lint",
|
|
||||||
]
|
|
||||||
+42
-23
@@ -61,8 +61,9 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface BackgroundWorkerFgHostApi {
|
interface BackgroundWorkerFgHostApi {
|
||||||
fun enable()
|
fun enableSyncWorker()
|
||||||
fun disable()
|
fun enableUploadWorker(callbackHandle: Long)
|
||||||
|
fun disableUploadWorker()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerFgHostApi. */
|
/** The codec used by BackgroundWorkerFgHostApi. */
|
||||||
@@ -74,11 +75,11 @@ interface BackgroundWorkerFgHostApi {
|
|||||||
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.enable()
|
api.enableSyncWorker()
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
@@ -90,11 +91,29 @@ interface BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val callbackHandleArg = args[0] as Long
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.enableUploadWorker(callbackHandleArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.disable()
|
api.disableUploadWorker()
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
@@ -111,7 +130,6 @@ interface BackgroundWorkerFgHostApi {
|
|||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface BackgroundWorkerBgHostApi {
|
interface BackgroundWorkerBgHostApi {
|
||||||
fun onInitialized()
|
fun onInitialized()
|
||||||
fun close()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerBgHostApi. */
|
/** The codec used by BackgroundWorkerBgHostApi. */
|
||||||
@@ -138,22 +156,6 @@ interface BackgroundWorkerBgHostApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
api.close()
|
|
||||||
listOf(null)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +167,23 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
|||||||
BackgroundWorkerPigeonCodec()
|
BackgroundWorkerPigeonCodec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun onLocalSync(maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(maxSecondsArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
|||||||
+43
-21
@@ -11,11 +11,17 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import io.flutter.FlutterInjector
|
import io.flutter.FlutterInjector
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor.DartCallback
|
||||||
import io.flutter.embedding.engine.loader.FlutterLoader
|
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||||
|
import io.flutter.view.FlutterCallbackInformation
|
||||||
|
|
||||||
private const val TAG = "BackgroundWorker"
|
private const val TAG = "BackgroundWorker"
|
||||||
|
|
||||||
|
enum class BackgroundTaskType {
|
||||||
|
LOCAL_SYNC,
|
||||||
|
UPLOAD,
|
||||||
|
}
|
||||||
|
|
||||||
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
@@ -52,6 +58,25 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
|
// Retrieve the callback handle stored by the main Flutter app
|
||||||
|
// This handle points to the Flutter function that should be executed in the background
|
||||||
|
val callbackHandle =
|
||||||
|
ctx.getSharedPreferences(BackgroundWorkerApiImpl.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getLong(BackgroundWorkerApiImpl.SHARED_PREF_CALLBACK_HANDLE, 0L)
|
||||||
|
|
||||||
|
if (callbackHandle == 0L) {
|
||||||
|
// Without a valid callback handle, we cannot start the Flutter background execution
|
||||||
|
complete(Result.failure())
|
||||||
|
return@ensureInitializationCompleteAsync
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the Flutter engine with the specified callback as the entry point
|
||||||
|
val callback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
|
||||||
|
if (callback == null) {
|
||||||
|
complete(Result.failure())
|
||||||
|
return@ensureInitializationCompleteAsync
|
||||||
|
}
|
||||||
|
|
||||||
// Register custom plugins
|
// Register custom plugins
|
||||||
MainActivity.registerPlugins(ctx, engine!!)
|
MainActivity.registerPlugins(ctx, engine!!)
|
||||||
flutterApi =
|
flutterApi =
|
||||||
@@ -61,12 +86,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
api = this
|
api = this
|
||||||
)
|
)
|
||||||
|
|
||||||
engine!!.dartExecutor.executeDartEntrypoint(
|
engine!!.dartExecutor.executeDartCallback(
|
||||||
DartExecutor.DartEntrypoint(
|
DartCallback(ctx.assets, loader.findAppBundlePath(), callback)
|
||||||
loader.findAppBundlePath(),
|
|
||||||
"package:immich_mobile/domain/services/background_worker.service.dart",
|
|
||||||
"backgroundSyncNativeEntrypoint"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +100,23 @@ 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) }
|
val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0)
|
||||||
|
val taskType = BackgroundTaskType.entries[taskTypeIndex]
|
||||||
|
|
||||||
|
when (taskType) {
|
||||||
|
BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) }
|
||||||
|
BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
/**
|
||||||
|
* Called when the system has to stop this worker because constraints are
|
||||||
|
* no longer met or the system needs resources for more important tasks
|
||||||
|
* This is also called when the worker has been explicitly cancelled or replaced
|
||||||
|
*/
|
||||||
|
override fun onStopped() {
|
||||||
|
Log.d(TAG, "About to stop BackupWorker")
|
||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,16 +134,6 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the system has to stop this worker because constraints are
|
|
||||||
* no longer met or the system needs resources for more important tasks
|
|
||||||
* This is also called when the worker has been explicitly cancelled or replaced
|
|
||||||
*/
|
|
||||||
override fun onStopped() {
|
|
||||||
Log.d(TAG, "About to stop BackupWorker")
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleHostResult(result: kotlin.Result<Unit>) {
|
private fun handleHostResult(result: kotlin.Result<Unit>) {
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
return
|
return
|
||||||
@@ -130,10 +154,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
* - Parameter success: Indicates whether the background task completed successfully
|
* - Parameter success: Indicates whether the background task completed successfully
|
||||||
*/
|
*/
|
||||||
private fun complete(success: Result) {
|
private fun complete(success: Result) {
|
||||||
Log.d(TAG, "About to complete BackupWorker with result: $success")
|
|
||||||
isComplete = true
|
isComplete = true
|
||||||
engine?.destroy()
|
engine?.destroy()
|
||||||
engine = null
|
|
||||||
flutterApi = null
|
flutterApi = null
|
||||||
completionHandler.set(success)
|
completionHandler.set(success)
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-8
@@ -3,8 +3,10 @@ package app.alextran.immich.background
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.Data
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@@ -14,13 +16,19 @@ private const val TAG = "BackgroundUploadImpl"
|
|||||||
|
|
||||||
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
override fun enableSyncWorker() {
|
||||||
override fun enable() {
|
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
|
Log.i(TAG, "Scheduled media observer")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disable() {
|
override fun enableUploadWorker(callbackHandle: Long) {
|
||||||
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
|
updateUploadEnabled(ctx, true)
|
||||||
|
updateCallbackHandle(ctx, callbackHandle)
|
||||||
|
Log.i(TAG, "Scheduled background upload tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableUploadWorker() {
|
||||||
|
updateUploadEnabled(ctx, false)
|
||||||
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
Log.i(TAG, "Cancelled background upload tasks")
|
Log.i(TAG, "Cancelled background upload tasks")
|
||||||
}
|
}
|
||||||
@@ -29,14 +37,32 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||||
|
|
||||||
|
const val WORKER_DATA_TASK_TYPE = "taskType"
|
||||||
|
|
||||||
|
const val SHARED_PREF_NAME = "Immich::Background"
|
||||||
|
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled"
|
||||||
|
const val SHARED_PREF_CALLBACK_HANDLE = "Background::backup::callbackHandle"
|
||||||
|
|
||||||
|
private fun updateUploadEnabled(context: Context, enabled: Boolean) {
|
||||||
|
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
|
||||||
|
putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCallbackHandle(context: Context, callbackHandle: Long) {
|
||||||
|
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
|
||||||
|
putLong(SHARED_PREF_CALLBACK_HANDLE, callbackHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun enqueueMediaObserver(ctx: Context) {
|
fun enqueueMediaObserver(ctx: Context) {
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
|
.setTriggerContentUpdateDelay(5, TimeUnit.SECONDS)
|
||||||
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
|
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
||||||
@@ -48,13 +74,15 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enqueueBackgroundWorker(ctx: Context) {
|
fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) {
|
||||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||||
|
|
||||||
|
val data = Data.Builder()
|
||||||
|
data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal)
|
||||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||||
.build()
|
.setInputData(data.build()).build()
|
||||||
WorkManager.getInstance(ctx)
|
WorkManager.getInstance(ctx)
|
||||||
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
||||||
|
|
||||||
|
|||||||
+22
-10
@@ -6,17 +6,29 @@ import androidx.work.Worker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
Log.i("MediaObserver", "Content change detected, starting background worker")
|
Log.i("MediaObserver", "Content change detected, starting background worker")
|
||||||
// Re-enqueue itself to listen for future changes
|
|
||||||
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
|
||||||
|
|
||||||
// Enqueue backup worker only if there are new media changes
|
// Enqueue backup worker only if there are new media changes
|
||||||
if (triggeredContentUris.isNotEmpty()) {
|
if (triggeredContentUris.isNotEmpty()) {
|
||||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
val type =
|
||||||
|
if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC
|
||||||
|
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enqueue itself to listen for future changes
|
||||||
|
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBackupEnabled(context: Context): Boolean {
|
||||||
|
val prefs =
|
||||||
|
context.getSharedPreferences(
|
||||||
|
BackgroundWorkerApiImpl.SHARED_PREF_NAME,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false)
|
||||||
}
|
}
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- cupertino_http (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.9):
|
- DKImagePickerController/Core (4.3.9):
|
||||||
@@ -77,6 +80,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- network_info_plus (0.0.1):
|
- network_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- objective_c (0.0.1):
|
||||||
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
@@ -136,6 +141,7 @@ DEPENDENCIES:
|
|||||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
|
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
@@ -154,6 +160,7 @@ DEPENDENCIES:
|
|||||||
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
||||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||||
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
@@ -184,6 +191,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
|
cupertino_http:
|
||||||
|
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
@@ -220,6 +229,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/native_video_player/ios"
|
:path: ".symlinks/plugins/native_video_player/ios"
|
||||||
network_info_plus:
|
network_info_plus:
|
||||||
:path: ".symlinks/plugins/network_info_plus/ios"
|
:path: ".symlinks/plugins/network_info_plus/ios"
|
||||||
|
objective_c:
|
||||||
|
:path: ".symlinks/plugins/objective_c/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
@@ -249,6 +260,7 @@ SPEC CHECKSUMS:
|
|||||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||||
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
|
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
@@ -270,6 +282,7 @@ SPEC CHECKSUMS:
|
|||||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
||||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||||
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -507,10 +507,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -539,10 +543,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import UIKit
|
|||||||
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
|
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
|
||||||
|
|
||||||
BackgroundServicePlugin.registerBackgroundProcessing()
|
BackgroundServicePlugin.registerBackgroundProcessing()
|
||||||
BackgroundWorkerApiImpl.registerBackgroundWorkers()
|
BackgroundWorkerApiImpl.registerBackgroundProcessing()
|
||||||
|
|
||||||
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||||
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
|
|||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol BackgroundWorkerFgHostApi {
|
protocol BackgroundWorkerFgHostApi {
|
||||||
func enable() throws
|
func enableSyncWorker() throws
|
||||||
func disable() throws
|
func enableUploadWorker(callbackHandle: Int64) throws
|
||||||
|
func disableUploadWorker() throws
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -83,38 +84,52 @@ class BackgroundWorkerFgHostApiSetup {
|
|||||||
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
enableChannel.setMessageHandler { _, reply in
|
enableSyncWorkerChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
try api.enable()
|
try api.enableSyncWorker()
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableChannel.setMessageHandler(nil)
|
enableSyncWorkerChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
disableChannel.setMessageHandler { _, reply in
|
enableUploadWorkerChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let callbackHandleArg = args[0] as! Int64
|
||||||
do {
|
do {
|
||||||
try api.disable()
|
try api.enableUploadWorker(callbackHandle: callbackHandleArg)
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disableChannel.setMessageHandler(nil)
|
enableUploadWorkerChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
disableUploadWorkerChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.disableUploadWorker()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disableUploadWorkerChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol BackgroundWorkerBgHostApi {
|
protocol BackgroundWorkerBgHostApi {
|
||||||
func onInitialized() throws
|
func onInitialized() throws
|
||||||
func close() throws
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -136,23 +151,11 @@ class BackgroundWorkerBgHostApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
onInitializedChannel.setMessageHandler(nil)
|
onInitializedChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
closeChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
try api.close()
|
|
||||||
reply(wrapResult(nil))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
closeChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 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 onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
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(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
@@ -167,6 +170,24 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
|||||||
var codec: BackgroundWorkerPigeonCodec {
|
var codec: BackgroundWorkerPigeonCodec {
|
||||||
return BackgroundWorkerPigeonCodec.shared
|
return BackgroundWorkerPigeonCodec.shared
|
||||||
}
|
}
|
||||||
|
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)"
|
||||||
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([maxSecondsArg] as [Any?]) { response in
|
||||||
|
guard let listResponse = response as? [Any?] else {
|
||||||
|
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if listResponse.count > 1 {
|
||||||
|
let code: String = listResponse[0] as! String
|
||||||
|
let message: String? = nilOrValue(listResponse[1])
|
||||||
|
let details: String? = nilOrValue(listResponse[2])
|
||||||
|
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
||||||
|
} else {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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) {
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
enum BackgroundTaskType { case refresh, processing }
|
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DEBUG: Testing Background Tasks in Xcode
|
* DEBUG: Testing Background Tasks in Xcode
|
||||||
@@ -9,6 +9,10 @@ enum BackgroundTaskType { case refresh, processing }
|
|||||||
* To test background task functionality during development:
|
* To test background task functionality during development:
|
||||||
* 1. Pause the application in Xcode debugger
|
* 1. Pause the application in Xcode debugger
|
||||||
* 2. In the debugger console, enter one of the following commands:
|
* 2. In the debugger console, enter one of the following commands:
|
||||||
|
|
||||||
|
## For local sync (short-running sync):
|
||||||
|
|
||||||
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||||
|
|
||||||
## For background refresh (short-running sync):
|
## For background refresh (short-running sync):
|
||||||
|
|
||||||
@@ -20,6 +24,8 @@ enum BackgroundTaskType { case refresh, processing }
|
|||||||
|
|
||||||
* To simulate task expiration (useful for testing expiration handlers):
|
* To simulate task expiration (useful for testing expiration handlers):
|
||||||
|
|
||||||
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
||||||
@@ -80,10 +86,28 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
* starts the engine, and sets up a timeout timer if specified.
|
* starts the engine, and sets up a timeout timer if specified.
|
||||||
*/
|
*/
|
||||||
func run() {
|
func run() {
|
||||||
|
// Retrieve the callback handle stored by the main Flutter app
|
||||||
|
// This handle points to the Flutter function that should be executed in the background
|
||||||
|
let callbackHandle = Int64(UserDefaults.standard.string(
|
||||||
|
forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) ?? "0") ?? 0
|
||||||
|
|
||||||
|
if callbackHandle == 0 {
|
||||||
|
// Without a valid callback handle, we cannot start the Flutter background execution
|
||||||
|
complete(success: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the callback handle to retrieve the actual Flutter callback information
|
||||||
|
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
|
||||||
|
// The callback handle is invalid or the callback was not found
|
||||||
|
complete(success: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Start the Flutter engine with the specified callback as the entry point
|
// Start the Flutter engine with the specified callback as the entry point
|
||||||
let isRunning = engine.run(
|
let isRunning = engine.run(
|
||||||
withEntrypoint: "backgroundSyncNativeEntrypoint",
|
withEntrypoint: callback.callbackName,
|
||||||
libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart"
|
libraryURI: callback.callbackLibraryPath
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify that the Flutter engine started successfully
|
// Verify that the Flutter engine started successfully
|
||||||
@@ -103,7 +127,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
if maxSeconds != nil {
|
if maxSeconds != nil {
|
||||||
// Schedule a timer to cancel the task after the specified timeout period
|
// Schedule a timer to cancel the task after the specified timeout period
|
||||||
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
|
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
|
||||||
self.close()
|
self.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,17 +138,25 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
func onInitialized() throws {
|
func onInitialized() throws {
|
||||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
switch self.taskType {
|
||||||
self.handleHostResult(result: result)
|
case .refreshUpload, .processingUpload:
|
||||||
})
|
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
|
||||||
|
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||||
|
self.handleHostResult(result: result)
|
||||||
|
})
|
||||||
|
case .localSync:
|
||||||
|
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||||
|
self.handleHostResult(result: result)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the currently running background task, either due to timeout or external request.
|
* Cancels the currently running background task, either due to timeout or external request.
|
||||||
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||||
* the completion handler is eventually called even if Flutter doesn't respond.
|
* the completion handler is eventually called even if Flutter doesn't respond.
|
||||||
*/
|
*/
|
||||||
func close() {
|
func cancel() {
|
||||||
if isComplete {
|
if isComplete {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -140,7 +172,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
self.complete(success: false)
|
self.complete(success: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the result from Flutter API calls and determines the success/failure status.
|
* Handles the result from Flutter API calls and determines the success/failure status.
|
||||||
@@ -151,7 +182,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
private func handleHostResult(result: Result<Void, PigeonError>) {
|
private func handleHostResult(result: Result<Void, PigeonError>) {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(): self.complete(success: true)
|
case .success(): self.complete(success: true)
|
||||||
case .failure(_): self.close()
|
case .failure(_): self.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,10 +195,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
* - Parameter success: Indicates whether the background task completed successfully
|
* - Parameter success: Indicates whether the background task completed successfully
|
||||||
*/
|
*/
|
||||||
private func complete(success: Bool) {
|
private func complete(success: Bool) {
|
||||||
if(isComplete) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isComplete = true
|
isComplete = true
|
||||||
engine.destroyContext()
|
engine.destroyContext()
|
||||||
completionHandler(success)
|
completionHandler(success)
|
||||||
|
|||||||
@@ -1,40 +1,84 @@
|
|||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
|
|
||||||
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
|
func enableSyncWorker() throws {
|
||||||
func enable() throws {
|
BackgroundWorkerApiImpl.scheduleLocalSync()
|
||||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
|
||||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
|
||||||
print("BackgroundUploadImpl:enbale Background worker scheduled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func disable() throws {
|
func enableUploadWorker(callbackHandle: Int64) throws {
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
BackgroundWorkerApiImpl.updateUploadEnabled(true)
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
// Store the callback handle for later use when starting background Flutter isolates
|
||||||
print("BackgroundUploadImpl:disableUploadWorker Disabled background workers")
|
BackgroundWorkerApiImpl.updateUploadCallbackHandle(callbackHandle)
|
||||||
|
|
||||||
|
BackgroundWorkerApiImpl.scheduleRefreshUpload()
|
||||||
|
BackgroundWorkerApiImpl.scheduleProcessingUpload()
|
||||||
|
print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
func disableUploadWorker() throws {
|
||||||
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||||
|
BackgroundWorkerApiImpl.cancelUploadTasks()
|
||||||
|
print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled"
|
||||||
|
public static let backgroundUploadCallbackHandleKey = "immich:background:backup:callbackHandle"
|
||||||
|
|
||||||
|
private static let localSyncTaskID = "app.alextran.immich.background.localSync"
|
||||||
|
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
|
||||||
|
private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload"
|
||||||
|
|
||||||
public static func registerBackgroundWorkers() {
|
private static func updateUploadEnabled(_ isEnabled: Bool) {
|
||||||
|
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func updateUploadCallbackHandle(_ callbackHandle: Int64) {
|
||||||
|
return UserDefaults.standard.set(String(callbackHandle), forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func cancelUploadTasks() {
|
||||||
|
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func registerBackgroundProcessing() {
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
|
||||||
if task is BGProcessingTask {
|
if task is BGProcessingTask {
|
||||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in
|
||||||
if task is BGAppRefreshTask {
|
if task is BGAppRefreshTask {
|
||||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BGTaskScheduler.shared.register(
|
||||||
|
forTaskWithIdentifier: localSyncTaskID, using: nil) { task in
|
||||||
|
if task is BGAppRefreshTask {
|
||||||
|
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scheduleLocalSync() {
|
||||||
|
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID)
|
||||||
|
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
||||||
|
} catch {
|
||||||
|
print("Could not schedule the local sync task \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleRefreshWorker() {
|
private static func scheduleRefreshUpload() {
|
||||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID)
|
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID)
|
||||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -44,8 +88,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleProcessingWorker() {
|
private static func scheduleProcessingUpload() {
|
||||||
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID)
|
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID)
|
||||||
|
|
||||||
backgroundProcessing.requiresNetworkConnectivity = true
|
backgroundProcessing.requiresNetworkConnectivity = true
|
||||||
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
||||||
@@ -57,16 +101,16 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
|
||||||
scheduleRefreshWorker()
|
scheduleRefreshUpload()
|
||||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
// Restrict the refresh task to run only for a maximum of 20 seconds
|
||||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||||
scheduleProcessingWorker()
|
scheduleProcessingUpload()
|
||||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +134,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
|||||||
|
|
||||||
task.expirationHandler = {
|
task.expirationHandler = {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
backgroundWorker.close()
|
backgroundWorker.cancel()
|
||||||
}
|
}
|
||||||
isSuccess = false
|
isSuccess = false
|
||||||
|
|
||||||
|
|||||||
@@ -46,23 +46,6 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
assetCache.countLimit = 10000
|
assetCache.countLimit = 10000
|
||||||
return assetCache
|
return assetCache
|
||||||
}()
|
}()
|
||||||
private static let activitySemaphore = DispatchSemaphore(value: 1)
|
|
||||||
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
|
|
||||||
forName: UIApplication.willResignActiveNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { _ in
|
|
||||||
processingQueue.suspend()
|
|
||||||
activitySemaphore.wait()
|
|
||||||
}
|
|
||||||
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
|
|
||||||
forName: UIApplication.didBecomeActiveNotification,
|
|
||||||
object: nil,
|
|
||||||
queue: .main
|
|
||||||
) { _ in
|
|
||||||
processingQueue.resume()
|
|
||||||
activitySemaphore.signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||||
Self.processingQueue.async {
|
Self.processingQueue.async {
|
||||||
@@ -70,7 +53,6 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||||
|
|
||||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
||||||
self.waitForActiveState()
|
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +142,6 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
return completion(Self.cancelledResult)
|
return completion(Self.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.waitForActiveState()
|
|
||||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
||||||
Self.removeRequest(requestId: requestId)
|
Self.removeRequest(requestId: requestId)
|
||||||
}
|
}
|
||||||
@@ -203,9 +184,4 @@ class ThumbnailApiImpl: ThumbnailApi {
|
|||||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||||
return asset
|
return asset
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForActiveState() {
|
|
||||||
Self.activitySemaphore.wait()
|
|
||||||
Self.activitySemaphore.signal()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-185
@@ -1,189 +1,190 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.background.refreshUpload</string>
|
<string>app.alextran.immich.background.localSync</string>
|
||||||
<string>app.alextran.immich.background.processingUpload</string>
|
<string>app.alextran.immich.background.refreshUpload</string>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.background.processingUpload</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
</array>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
</array>
|
||||||
<true/>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<true />
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<key>CFBundleDisplayName</key>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<key>CFBundleDisplayName</key>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<array>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<dict>
|
<array>
|
||||||
<key>CFBundleTypeName</key>
|
<dict>
|
||||||
<string>ShareHandler</string>
|
<key>CFBundleTypeName</key>
|
||||||
<key>LSHandlerRank</key>
|
<string>ShareHandler</string>
|
||||||
<string>Alternate</string>
|
<key>LSHandlerRank</key>
|
||||||
<key>LSItemContentTypes</key>
|
<string>Alternate</string>
|
||||||
<array>
|
<key>LSItemContentTypes</key>
|
||||||
<string>public.file-url</string>
|
<array>
|
||||||
<string>public.image</string>
|
<string>public.file-url</string>
|
||||||
<string>public.text</string>
|
<string>public.image</string>
|
||||||
<string>public.movie</string>
|
<string>public.text</string>
|
||||||
<string>public.url</string>
|
<string>public.movie</string>
|
||||||
<string>public.data</string>
|
<string>public.url</string>
|
||||||
</array>
|
<string>public.data</string>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
<key>CFBundleExecutable</key>
|
</array>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<key>CFBundleExecutable</key>
|
||||||
<key>CFBundleIdentifier</key>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<key>CFBundleIdentifier</key>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>6.0</string>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<key>CFBundleLocalizations</key>
|
<string>6.0</string>
|
||||||
<array>
|
<key>CFBundleLocalizations</key>
|
||||||
<string>en</string>
|
<array>
|
||||||
<string>ar</string>
|
<string>en</string>
|
||||||
<string>ca</string>
|
<string>ar</string>
|
||||||
<string>cs</string>
|
<string>ca</string>
|
||||||
<string>da</string>
|
<string>cs</string>
|
||||||
<string>de</string>
|
<string>da</string>
|
||||||
<string>es</string>
|
<string>de</string>
|
||||||
<string>fi</string>
|
<string>es</string>
|
||||||
<string>fr</string>
|
<string>fi</string>
|
||||||
<string>he</string>
|
<string>fr</string>
|
||||||
<string>hi</string>
|
<string>he</string>
|
||||||
<string>hu</string>
|
<string>hi</string>
|
||||||
<string>it</string>
|
<string>hu</string>
|
||||||
<string>ja</string>
|
<string>it</string>
|
||||||
<string>ko</string>
|
<string>ja</string>
|
||||||
<string>lv</string>
|
<string>ko</string>
|
||||||
<string>mn</string>
|
<string>lv</string>
|
||||||
<string>nb</string>
|
<string>mn</string>
|
||||||
<string>nl</string>
|
<string>nb</string>
|
||||||
<string>pl</string>
|
<string>nl</string>
|
||||||
<string>pt</string>
|
<string>pl</string>
|
||||||
<string>ro</string>
|
<string>pt</string>
|
||||||
<string>ru</string>
|
<string>ro</string>
|
||||||
<string>sk</string>
|
<string>ru</string>
|
||||||
<string>sl</string>
|
<string>sk</string>
|
||||||
<string>sr</string>
|
<string>sl</string>
|
||||||
<string>sv</string>
|
<string>sr</string>
|
||||||
<string>th</string>
|
<string>sv</string>
|
||||||
<string>uk</string>
|
<string>th</string>
|
||||||
<string>vi</string>
|
<string>uk</string>
|
||||||
<string>zh</string>
|
<string>vi</string>
|
||||||
</array>
|
<string>zh</string>
|
||||||
<key>CFBundleName</key>
|
</array>
|
||||||
<string>immich_mobile</string>
|
<key>CFBundleName</key>
|
||||||
<key>CFBundlePackageType</key>
|
<string>immich_mobile</string>
|
||||||
<string>APPL</string>
|
<key>CFBundlePackageType</key>
|
||||||
<key>CFBundleShortVersionString</key>
|
<string>APPL</string>
|
||||||
<string>1.140.0</string>
|
<key>CFBundleShortVersionString</key>
|
||||||
<key>CFBundleSignature</key>
|
<string>1.140.0</string>
|
||||||
<string>????</string>
|
<key>CFBundleSignature</key>
|
||||||
<key>CFBundleURLTypes</key>
|
<string>????</string>
|
||||||
<array>
|
<key>CFBundleURLTypes</key>
|
||||||
<dict>
|
<array>
|
||||||
<key>CFBundleTypeRole</key>
|
<dict>
|
||||||
<string>Editor</string>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Editor</string>
|
||||||
<string>Share Extension</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<string>Share Extension</string>
|
||||||
<array>
|
<key>CFBundleURLSchemes</key>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<array>
|
||||||
</array>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</dict>
|
</array>
|
||||||
<dict>
|
</dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<dict>
|
||||||
<string>Editor</string>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Editor</string>
|
||||||
<string>Deep Link</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<string>Deep Link</string>
|
||||||
<array>
|
<key>CFBundleURLSchemes</key>
|
||||||
<string>immich</string>
|
<array>
|
||||||
</array>
|
<string>immich</string>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
<key>CFBundleVersion</key>
|
</array>
|
||||||
<string>219</string>
|
<key>CFBundleVersion</key>
|
||||||
<key>FLTEnableImpeller</key>
|
<string>219</string>
|
||||||
<true/>
|
<key>FLTEnableImpeller</key>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<true />
|
||||||
<false/>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<false />
|
||||||
<array>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<string>https</string>
|
<array>
|
||||||
</array>
|
<string>https</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
</array>
|
||||||
<true/>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<true />
|
||||||
<string>No</string>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<string>No</string>
|
||||||
<true/>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<key>NSAppTransportSecurity</key>
|
<true />
|
||||||
<dict>
|
<key>NSAppTransportSecurity</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<dict>
|
||||||
<true/>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
</dict>
|
<true />
|
||||||
<key>NSBonjourServices</key>
|
</dict>
|
||||||
<array>
|
<key>NSBonjourServices</key>
|
||||||
<string>_googlecast._tcp</string>
|
<array>
|
||||||
<string>_CC1AD845._googlecast._tcp</string>
|
<string>_googlecast._tcp</string>
|
||||||
</array>
|
<string>_CC1AD845._googlecast._tcp</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
</array>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
<string>We need local network permission to connect to the local server using IP address and
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>We need local network permission to connect to the local server using IP address and
|
||||||
allow the casting feature to work</string>
|
allow the casting feature to work</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
<key>NSLocationUsageDescription</key>
|
<key>NSLocationUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>processing</string>
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -5,7 +5,6 @@ import 'package:background_downloader/background_downloader.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||||
@@ -15,7 +14,6 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/auth.service.dart';
|
import 'package:immich_mobile/services/auth.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
@@ -31,9 +29,13 @@ class BackgroundWorkerFgService {
|
|||||||
const BackgroundWorkerFgService(this._foregroundHostApi);
|
const BackgroundWorkerFgService(this._foregroundHostApi);
|
||||||
|
|
||||||
// TODO: Move this call to native side once old timeline is removed
|
// TODO: Move this call to native side once old timeline is removed
|
||||||
Future<void> enable() => _foregroundHostApi.enable();
|
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
|
||||||
|
|
||||||
Future<void> disable() => _foregroundHostApi.disable();
|
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker(
|
||||||
|
PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
@@ -43,7 +45,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
final DriftLogger _driftLogger;
|
final DriftLogger _driftLogger;
|
||||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||||
final Logger _logger = Logger('BackgroundUploadBgService');
|
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||||
late final IsolateLockManager _lockManager;
|
|
||||||
|
|
||||||
bool _isCleanedUp = false;
|
bool _isCleanedUp = false;
|
||||||
|
|
||||||
@@ -59,106 +60,98 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
driftProvider.overrideWith(driftOverride(drift)),
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
|
|
||||||
BackgroundWorkerFlutterApi.setUp(this);
|
BackgroundWorkerFlutterApi.setUp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
try {
|
await loadTranslations();
|
||||||
await loadTranslations();
|
HttpSSLOptions.apply(applyNative: false);
|
||||||
HttpSSLOptions.apply(applyNative: false);
|
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
||||||
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
|
||||||
|
|
||||||
// Initialize the file downloader
|
// Initialize the file downloader
|
||||||
await FileDownloader().configure(
|
await FileDownloader().configure(
|
||||||
globalConfig: [
|
globalConfig: [
|
||||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||||
(Config.holdingQueue, (6, 6, 3)),
|
(Config.holdingQueue, (6, 6, 3)),
|
||||||
// On Android, if files are larger than 256MB, run in foreground service
|
// On Android, if files are larger than 256MB, run in foreground service
|
||||||
(Config.runInForegroundIfFileLargerThan, 256),
|
(Config.runInForegroundIfFileLargerThan, 256),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||||
await FileDownloader().trackTasks();
|
await FileDownloader().trackTasks();
|
||||||
configureFileDownloaderNotifications();
|
configureFileDownloaderNotifications();
|
||||||
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
|
||||||
|
|
||||||
// Notify the host that the background upload service has been initialized and is ready to use
|
// Notify the host that the background upload service has been initialized and is ready to use
|
||||||
debugPrint("Acquiring background worker lock");
|
await _backgroundHostApi.onInitialized();
|
||||||
if (await _lockManager.acquireLock().timeout(
|
|
||||||
const Duration(seconds: 5),
|
|
||||||
onTimeout: () {
|
|
||||||
_lockManager.cancel();
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
)) {
|
|
||||||
_logger.info("Acquired background worker lock");
|
|
||||||
await _backgroundHostApi.onInitialized();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.warning("Failed to acquire background worker lock");
|
|
||||||
await _cleanup();
|
|
||||||
await _backgroundHostApi.close();
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to initialize background worker", error, stack);
|
|
||||||
_backgroundHostApi.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLocalSync(int? maxSeconds) async {
|
||||||
|
_logger.info('Local background syncing started');
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
|
||||||
|
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
|
||||||
|
await _syncAssets(hashTimeout: timeout, syncRemote: false);
|
||||||
|
|
||||||
|
sw.stop();
|
||||||
|
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We do the following on Android upload
|
||||||
|
* - Sync local assets
|
||||||
|
* - Hash local assets 3 / 6 minutes
|
||||||
|
* - Sync remote assets
|
||||||
|
* - Check and requeue upload tasks
|
||||||
|
*/
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload() async {
|
||||||
try {
|
_logger.info('Android background processing started');
|
||||||
_logger.info('Android background processing started');
|
final sw = Stopwatch()..start();
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
|
|
||||||
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
||||||
await _handleBackup(processBulk: false);
|
await _handleBackup(processBulk: false);
|
||||||
|
|
||||||
sw.stop();
|
await _cleanup();
|
||||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
} catch (error, stack) {
|
sw.stop();
|
||||||
_logger.severe("Failed to complete Android background processing", error, stack);
|
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
||||||
} finally {
|
|
||||||
await _cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* We do the following on background upload
|
||||||
|
* - Sync local assets
|
||||||
|
* - Hash local assets
|
||||||
|
* - Sync remote assets
|
||||||
|
* - Check and requeue upload tasks
|
||||||
|
*
|
||||||
|
* The native side will not send the maxSeconds value for processing tasks
|
||||||
|
*/
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
try {
|
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
||||||
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
final sw = Stopwatch()..start();
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
|
|
||||||
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
await _syncAssets(hashTimeout: timeout);
|
await _syncAssets(hashTimeout: timeout);
|
||||||
|
|
||||||
final backupFuture = _handleBackup();
|
final backupFuture = _handleBackup();
|
||||||
if (maxSeconds != null) {
|
if (maxSeconds != null) {
|
||||||
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
||||||
} else {
|
} else {
|
||||||
await backupFuture;
|
await backupFuture;
|
||||||
}
|
|
||||||
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
|
||||||
} finally {
|
|
||||||
await _cleanup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _cleanup();
|
||||||
|
|
||||||
|
sw.stop();
|
||||||
|
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> cancel() async {
|
Future<void> cancel() async {
|
||||||
_logger.warning("Background worker cancelled");
|
_logger.warning("Background upload cancelled");
|
||||||
try {
|
await _cleanup();
|
||||||
await _cleanup();
|
|
||||||
} catch (error, stack) {
|
|
||||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanup() async {
|
Future<void> _cleanup() async {
|
||||||
@@ -166,22 +159,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
_isCleanedUp = true;
|
||||||
_isCleanedUp = true;
|
await _ref.read(backgroundSyncProvider).cancel();
|
||||||
_logger.info("Cleaning up background worker");
|
await _ref.read(backgroundSyncProvider).cancelLocal();
|
||||||
await _ref.read(backgroundSyncProvider).cancel();
|
await _isar.close();
|
||||||
await _ref.read(backgroundSyncProvider).cancelLocal();
|
await _drift.close();
|
||||||
if (_isar.isOpen) {
|
await _driftLogger.close();
|
||||||
await _isar.close();
|
_ref.dispose();
|
||||||
}
|
|
||||||
await _drift.close();
|
|
||||||
await _driftLogger.close();
|
|
||||||
_ref.dispose();
|
|
||||||
_lockManager.releaseLock();
|
|
||||||
_logger.info("Background worker resources cleaned up");
|
|
||||||
} catch (error, stack) {
|
|
||||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||||
@@ -206,7 +190,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
|
||||||
final futures = <Future<void>>[];
|
final futures = <Future<void>>[];
|
||||||
|
|
||||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||||
@@ -228,16 +212,17 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
futures.add(localSyncFuture);
|
futures.add(localSyncFuture);
|
||||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
if (syncRemote) {
|
||||||
|
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
futures.add(remoteSyncFuture);
|
||||||
|
}
|
||||||
|
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Native entry invoked from the background worker. If renaming or moving this to a different
|
|
||||||
/// library, make sure to update the entry points and URI in native workers as well
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> backgroundSyncNativeEntrypoint() async {
|
Future<void> _backgroundSyncNativeEntrypoint() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
@@ -36,7 +35,6 @@ class HashService {
|
|||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> hashAssets() async {
|
Future<void> hashAssets() async {
|
||||||
_log.info("Starting hashing of assets");
|
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// Sorted by backupSelection followed by isCloud
|
// Sorted by backupSelection followed by isCloud
|
||||||
final localAlbums = await _localAlbumRepository.getAll(
|
final localAlbums = await _localAlbumRepository.getAll(
|
||||||
@@ -51,7 +49,7 @@ class HashService {
|
|||||||
|
|
||||||
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
||||||
if (assetsToHash.isNotEmpty) {
|
if (assetsToHash.isNotEmpty) {
|
||||||
await _hashAssets(album, assetsToHash);
|
await _hashAssets(assetsToHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ class HashService {
|
|||||||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||||
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
|
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
|
||||||
int bytesProcessed = 0;
|
int bytesProcessed = 0;
|
||||||
final toHash = <_AssetToPath>[];
|
final toHash = <_AssetToPath>[];
|
||||||
|
|
||||||
@@ -74,9 +72,6 @@ class HashService {
|
|||||||
|
|
||||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_log.warning(
|
|
||||||
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}",
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,17 +79,17 @@ class HashService {
|
|||||||
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
||||||
|
|
||||||
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(toHash);
|
||||||
toHash.clear();
|
toHash.clear();
|
||||||
bytesProcessed = 0;
|
bytesProcessed = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(toHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a batch of assets.
|
/// Processes a batch of assets.
|
||||||
Future<void> _processBatch(LocalAlbum album, List<_AssetToPath> toHash) async {
|
Future<void> _processBatch(List<_AssetToPath> toHash) async {
|
||||||
if (toHash.isEmpty) {
|
if (toHash.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,9 +114,7 @@ class HashService {
|
|||||||
if (hash?.length == 20) {
|
if (hash?.length == 20) {
|
||||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
_log.warning(
|
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||||
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
@@ -286,7 +285,7 @@ extension on Iterable<PlatformAlbum> {
|
|||||||
(e) => LocalAlbum(
|
(e) => LocalAlbum(
|
||||||
id: e.id,
|
id: e.id,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
|
updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
|
||||||
assetCount: e.assetCount,
|
assetCount: e.assetCount,
|
||||||
),
|
),
|
||||||
).toList();
|
).toList();
|
||||||
@@ -301,8 +300,8 @@ extension on Iterable<PlatformAsset> {
|
|||||||
name: e.name,
|
name: e.name,
|
||||||
checksum: null,
|
checksum: null,
|
||||||
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
|
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
|
||||||
createdAt: tryFromSecondsSinceEpoch(e.createdAt) ?? DateTime.now(),
|
createdAt: e.createdAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000),
|
||||||
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
|
updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
|
||||||
width: e.width,
|
width: e.width,
|
||||||
height: e.height,
|
height: e.height,
|
||||||
durationInSeconds: e.durationInSeconds,
|
durationInSeconds: e.durationInSeconds,
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
import 'dart:isolate';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
const String kIsolateLockManagerPort = "immich://isolate_mutex";
|
|
||||||
|
|
||||||
enum _LockStatus { active, released }
|
|
||||||
|
|
||||||
class _IsolateRequest {
|
|
||||||
const _IsolateRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HeartbeatRequest extends _IsolateRequest {
|
|
||||||
// Port for the receiver to send replies back
|
|
||||||
final SendPort sendPort;
|
|
||||||
|
|
||||||
const _HeartbeatRequest(this.sendPort);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {'type': 'heartbeat', 'sendPort': sendPort};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CloseRequest extends _IsolateRequest {
|
|
||||||
const _CloseRequest();
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {'type': 'close'};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IsolateResponse {
|
|
||||||
const _IsolateResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HeartbeatResponse extends _IsolateResponse {
|
|
||||||
final _LockStatus status;
|
|
||||||
|
|
||||||
const _HeartbeatResponse(this.status);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {'type': 'heartbeat', 'status': status.index};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef OnCloseLockHolderRequest = void Function();
|
|
||||||
|
|
||||||
class IsolateLockManager {
|
|
||||||
final String _portName;
|
|
||||||
bool _hasLock = false;
|
|
||||||
ReceivePort? _receivePort;
|
|
||||||
final OnCloseLockHolderRequest? _onCloseRequest;
|
|
||||||
final Set<SendPort> _waitingIsolates = {};
|
|
||||||
// Token object - a new one is created for each acquisition attempt
|
|
||||||
Object? _currentAcquisitionToken;
|
|
||||||
|
|
||||||
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
|
|
||||||
: _portName = portName ?? kIsolateLockManagerPort,
|
|
||||||
_onCloseRequest = onCloseRequest;
|
|
||||||
|
|
||||||
Future<bool> acquireLock() async {
|
|
||||||
if (_hasLock) {
|
|
||||||
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new token - this invalidates any previous attempt
|
|
||||||
final token = _currentAcquisitionToken = Object();
|
|
||||||
|
|
||||||
final ReceivePort rp = _receivePort = ReceivePort(_portName);
|
|
||||||
final SendPort sp = rp.sendPort;
|
|
||||||
|
|
||||||
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
|
|
||||||
// This attempt was superseded by a newer one in the same isolate
|
|
||||||
if (_currentAcquisitionToken != token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _lockReleasedByHolder(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasLock = true;
|
|
||||||
rp.listen(_onRequest);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _lockReleasedByHolder(Object token) async {
|
|
||||||
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
|
|
||||||
debugPrint("Found lock holder: $holder");
|
|
||||||
if (holder == null) {
|
|
||||||
// No holder, try and acquire lock
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ReceivePort tempRp = ReceivePort();
|
|
||||||
final SendPort tempSp = tempRp.sendPort;
|
|
||||||
final bs = tempRp.asBroadcastStream();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
// Send a heartbeat request with the send port to receive reply from the holder
|
|
||||||
|
|
||||||
debugPrint("Sending heartbeat request to lock holder");
|
|
||||||
holder.send(_HeartbeatRequest(tempSp).toJson());
|
|
||||||
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
|
||||||
|
|
||||||
debugPrint("Received heartbeat response from lock holder: $answer");
|
|
||||||
// This attempt was superseded by a newer one in the same isolate
|
|
||||||
if (_currentAcquisitionToken != token) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answer == null) {
|
|
||||||
// Holder failed, most likely killed without calling releaseLock
|
|
||||||
// Check if a different waiting isolate took the lock
|
|
||||||
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
|
|
||||||
// No, remove the stale lock
|
|
||||||
IsolateNameServer.removePortNameMapping(_portName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown message type received for heartbeat request. Try again
|
|
||||||
_IsolateResponse? response = _parseResponse(answer);
|
|
||||||
if (response == null || response is! _HeartbeatResponse) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status == _LockStatus.released) {
|
|
||||||
// Holder has released the lock
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the _LockStatus is active, we check again if the task completed
|
|
||||||
// by sending a released messaged again, if not, send a new heartbeat again
|
|
||||||
|
|
||||||
// Check if the holder completed its task after the heartbeat
|
|
||||||
answer = await bs.first.timeout(
|
|
||||||
const Duration(seconds: 3),
|
|
||||||
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
|
|
||||||
);
|
|
||||||
|
|
||||||
response = _parseResponse(answer);
|
|
||||||
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Timeout or error
|
|
||||||
} finally {
|
|
||||||
tempRp.close();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_IsolateRequest? _parseRequest(dynamic msg) {
|
|
||||||
if (msg is! Map<String, dynamic>) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (msg['type']) {
|
|
||||||
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
|
|
||||||
'close' => const _CloseRequest(),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_IsolateResponse? _parseResponse(dynamic msg) {
|
|
||||||
if (msg is! Map<String, dynamic>) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (msg['type']) {
|
|
||||||
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executed in the isolate with the lock
|
|
||||||
void _onRequest(dynamic msg) {
|
|
||||||
final request = _parseRequest(msg);
|
|
||||||
if (request == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request is _HeartbeatRequest) {
|
|
||||||
// Add the send port to the list of waiting isolates
|
|
||||||
_waitingIsolates.add(request.sendPort);
|
|
||||||
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request is _CloseRequest) {
|
|
||||||
_onCloseRequest?.call();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void releaseLock() {
|
|
||||||
if (_hasLock) {
|
|
||||||
IsolateNameServer.removePortNameMapping(_portName);
|
|
||||||
|
|
||||||
// Notify waiting isolates
|
|
||||||
for (final port in _waitingIsolates) {
|
|
||||||
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
|
|
||||||
}
|
|
||||||
_waitingIsolates.clear();
|
|
||||||
|
|
||||||
_hasLock = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_receivePort?.close();
|
|
||||||
_receivePort = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancel() {
|
|
||||||
if (_hasLock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint("Cancelling ongoing acquire lock attempts");
|
|
||||||
// Create a new token to invalidate ongoing acquire lock attempts
|
|
||||||
_currentAcquisitionToken = Object();
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestHolderToClose() {
|
|
||||||
if (_hasLock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:cronet_http/cronet_http.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
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/providers/image/cache/remote_image_cache_manager.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
|
|
||||||
part 'local_image_request.dart';
|
part 'local_image_request.dart';
|
||||||
part 'thumbhash_image_request.dart';
|
part 'thumbhash_image_request.dart';
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
part of 'image_request.dart';
|
part of 'image_request.dart';
|
||||||
|
|
||||||
class RemoteImageRequest extends ImageRequest {
|
class RemoteImageRequest extends ImageRequest {
|
||||||
static final log = Logger('RemoteImageRequest');
|
static final _client = const NetworkRepository().getHttpClient(
|
||||||
static final client = HttpClient()..maxConnectionsPerHost = 16;
|
'thumbnails',
|
||||||
final RemoteCacheManager? cacheManager;
|
diskCapacity: kThumbnailDiskCacheSize,
|
||||||
|
memoryCapacity: 0,
|
||||||
|
maxConnections: 16,
|
||||||
|
cacheMode: CacheMode.disk,
|
||||||
|
);
|
||||||
final String uri;
|
final String uri;
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
HttpClientRequest? _request;
|
final abortTrigger = Completer<void>();
|
||||||
|
|
||||||
RemoteImageRequest({required this.uri, required this.headers, this.cacheManager});
|
RemoteImageRequest({required this.uri, required this.headers});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
|
||||||
@@ -16,15 +20,8 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled,
|
|
||||||
// so it ends up being a bottleneck. We only prefer fetching from it when it can skip the DB call.
|
|
||||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true);
|
|
||||||
if (cachedFileImage != null) {
|
|
||||||
return cachedFileImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final buffer = await _downloadImage(uri);
|
final buffer = await _downloadImage();
|
||||||
if (buffer == null) {
|
if (buffer == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -35,57 +32,41 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false);
|
|
||||||
if (cachedFileImage != null) {
|
|
||||||
return cachedFileImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
|
||||||
_request = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ImmutableBuffer?> _downloadImage(String url) async {
|
Future<ImmutableBuffer?> _downloadImage() async {
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final request = _request = await client.getUrl(Uri.parse(url));
|
final req = http.AbortableRequest('GET', Uri.parse(uri), abortTrigger: abortTrigger.future);
|
||||||
if (_isCancelled) {
|
req.headers.addAll(headers);
|
||||||
request.abort();
|
final res = await _client.send(req);
|
||||||
return _request = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in headers.entries) {
|
|
||||||
request.headers.set(entry.key, entry.value);
|
|
||||||
}
|
|
||||||
final response = await request.close();
|
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
|
_onCancelled();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final cacheManager = this.cacheManager;
|
if (res.statusCode != 200) {
|
||||||
final streamController = StreamController<List<int>>(sync: true);
|
throw Exception('Failed to download $uri: ${res.statusCode}');
|
||||||
final Stream<List<int>> stream;
|
}
|
||||||
cacheManager?.putStreamedFile(url, streamController.stream);
|
|
||||||
stream = response.map((chunk) {
|
final stream = res.stream.map((chunk) {
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
throw StateError('Cancelled request');
|
throw StateError('Cancelled request');
|
||||||
}
|
}
|
||||||
if (cacheManager != null) {
|
|
||||||
streamController.add(chunk);
|
|
||||||
}
|
|
||||||
return chunk;
|
return chunk;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Uint8List bytes = await _downloadBytes(stream, response.contentLength);
|
final Uint8List bytes = await _downloadBytes(stream, res.contentLength ?? -1);
|
||||||
streamController.close();
|
if (_isCancelled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await ImmutableBuffer.fromUint8List(bytes);
|
return await ImmutableBuffer.fromUint8List(bytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
streamController.addError(e);
|
|
||||||
streamController.close();
|
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -122,40 +103,6 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ImageInfo?> _loadCachedFile(
|
|
||||||
String url,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
double scale, {
|
|
||||||
required bool inMemoryOnly,
|
|
||||||
}) async {
|
|
||||||
final cacheManager = this.cacheManager;
|
|
||||||
if (_isCancelled || cacheManager == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url));
|
|
||||||
if (_isCancelled || file == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.file.path);
|
|
||||||
return await _decodeBuffer(buffer, decode, scale);
|
|
||||||
} catch (e) {
|
|
||||||
log.severe('Failed to decode cached image', e);
|
|
||||||
_evictFile(url);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _evictFile(String url) async {
|
|
||||||
try {
|
|
||||||
await cacheManager?.removeFile(url);
|
|
||||||
} catch (e) {
|
|
||||||
log.severe('Failed to remove cached image', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
|
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
buffer.dispose();
|
buffer.dispose();
|
||||||
@@ -173,7 +120,6 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void _onCancelled() {
|
void _onCancelled() {
|
||||||
_request?.abort();
|
abortTrigger.complete();
|
||||||
_request = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cronet_http/cronet_http.dart';
|
||||||
|
import 'package:cupertino_http/cupertino_http.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:immich_mobile/utils/user_agent.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class NetworkRepository {
|
||||||
|
static late Directory _cachePath;
|
||||||
|
static late String _userAgent;
|
||||||
|
static final _clients = <String, http.Client>{};
|
||||||
|
|
||||||
|
static Future<void> init() {
|
||||||
|
return (
|
||||||
|
getTemporaryDirectory().then((cachePath) => _cachePath = cachePath),
|
||||||
|
getUserAgentString().then((userAgent) => _userAgent = userAgent),
|
||||||
|
).wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset() {
|
||||||
|
Future.microtask(init);
|
||||||
|
for (final client in _clients.values) {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
_clients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const NetworkRepository();
|
||||||
|
|
||||||
|
http.Client getHttpClient(
|
||||||
|
String directoryName, {
|
||||||
|
int diskCapacity = 100 << 20,
|
||||||
|
int memoryCapacity = 10 << 20,
|
||||||
|
int maxConnections = 6,
|
||||||
|
CacheMode cacheMode = CacheMode.disk,
|
||||||
|
}) {
|
||||||
|
final cachedClient = _clients[directoryName];
|
||||||
|
if (cachedClient != null) {
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
final directory = Directory('${_cachePath.path}/$directoryName');
|
||||||
|
directory.createSync(recursive: true);
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final engine = CronetEngine.build(
|
||||||
|
cacheMode: cacheMode,
|
||||||
|
cacheMaxSize: diskCapacity,
|
||||||
|
storagePath: directory.path,
|
||||||
|
userAgent: _userAgent,
|
||||||
|
);
|
||||||
|
return _clients[directoryName] = CronetClient.fromCronetEngine(engine, closeEngine: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = URLSessionConfiguration.defaultSessionConfiguration()
|
||||||
|
..httpMaximumConnectionsPerHost = maxConnections
|
||||||
|
..cache = URLCache.withCapacity(
|
||||||
|
diskCapacity: diskCapacity,
|
||||||
|
memoryCapacity: memoryCapacity,
|
||||||
|
directory: directory.uri,
|
||||||
|
)
|
||||||
|
..httpAdditionalHeaders = {'User-Agent': _userAgent};
|
||||||
|
return _clients[directoryName] = CupertinoClient.fromSessionConfiguration(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,13 +16,6 @@ class StorageRepository {
|
|||||||
file = await entity?.originFile;
|
file = await entity?.originFile;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
log.warning("Cannot get file for asset $assetId");
|
log.warning("Cannot get file for asset $assetId");
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = await file.exists();
|
|
||||||
if (!exists) {
|
|
||||||
log.warning("File for asset $assetId does not exist");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log.warning("Error getting file for asset $assetId", error, stackTrace);
|
log.warning("Error getting file for asset $assetId", error, stackTrace);
|
||||||
@@ -41,13 +34,6 @@ class StorageRepository {
|
|||||||
log.warning(
|
log.warning(
|
||||||
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
);
|
);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = await file.exists();
|
|
||||||
if (!exists) {
|
|
||||||
log.warning("Motion file for asset ${asset.id} does not exist");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ 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/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.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:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SyncApiRepository {
|
class SyncApiRepository {
|
||||||
|
static final _client = const NetworkRepository().getHttpClient('api');
|
||||||
final Logger _logger = Logger('SyncApiRepository');
|
final Logger _logger = Logger('SyncApiRepository');
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
SyncApiRepository(this._api);
|
SyncApiRepository(this._api);
|
||||||
@@ -20,10 +22,8 @@ class SyncApiRepository {
|
|||||||
Future<void> streamChanges(
|
Future<void> streamChanges(
|
||||||
Function(List<SyncEvent>, Function() abort) onData, {
|
Function(List<SyncEvent>, Function() abort) onData, {
|
||||||
int batchSize = kSyncEventBatchSize,
|
int batchSize = kSyncEventBatchSize,
|
||||||
http.Client? httpClient,
|
|
||||||
}) async {
|
}) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final client = httpClient ?? http.Client();
|
|
||||||
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
final endpoint = "${_api.apiClient.basePath}/sync/stream";
|
||||||
|
|
||||||
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
||||||
@@ -70,7 +70,7 @@ class SyncApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await client.send(request);
|
final response = await _client.send(request);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
final errorBody = await response.stream.bytesToString();
|
final errorBody = await response.stream.bytesToString();
|
||||||
@@ -101,8 +101,6 @@ class SyncApiRepository {
|
|||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Error processing stream", error, stack);
|
_logger.severe("Error processing stream", error, stack);
|
||||||
return Future.error(error, stack);
|
return Future.error(error, stack);
|
||||||
} finally {
|
|
||||||
client.close();
|
|
||||||
}
|
}
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
|
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
|
||||||
|
|||||||
+19
-3
@@ -15,7 +15,9 @@ import 'package:immich_mobile/constants/locales.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -25,6 +27,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
@@ -112,6 +115,8 @@ Future<void> initApp() async {
|
|||||||
yield LicenseEntryWithLineBreaks([license.key], license.value);
|
yield LicenseEntryWithLineBreaks([license.key], license.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await NetworkRepository.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
@@ -204,11 +209,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// needs to be delayed so that EasyLocalization is working
|
// needs to be delayed so that EasyLocalization is working
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
ref.read(backgroundServiceProvider).disableService();
|
ref.read(driftBackgroundUploadFgService).enableSyncService();
|
||||||
ref.read(driftBackgroundUploadFgService).enable();
|
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
||||||
|
ref.read(backgroundServiceProvider).disableService();
|
||||||
|
ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ref.read(driftBackgroundUploadFgService).disable();
|
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
|
ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,6 +229,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
if (kDebugMode) {
|
||||||
|
NetworkRepository.reset();
|
||||||
|
}
|
||||||
|
super.reassemble();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -42,10 +43,12 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
|
|
||||||
await ref.read(backgroundSyncProvider).syncRemote();
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
|
await ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopBackup() async {
|
Future<void> stopBackup() async {
|
||||||
|
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
await ref.read(driftBackupProvider.notifier).cancel();
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
await ref.read(driftBackgroundUploadFgService).disable();
|
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
}
|
}
|
||||||
|
|
||||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@@ -23,23 +21,14 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
|
|||||||
|
|
||||||
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||||
final log = Logger("SplashScreenPage");
|
final log = Logger("SplashScreenPage");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
ref
|
||||||
|
.read(authProvider.notifier)
|
||||||
lockManager.requestHolderToClose();
|
.setOpenApiServiceEndpoint()
|
||||||
lockManager
|
.then(logConnectionInfo)
|
||||||
.acquireLock()
|
.whenComplete(() => resumeSession());
|
||||||
.timeout(const Duration(seconds: 5))
|
|
||||||
.whenComplete(
|
|
||||||
() => ref
|
|
||||||
.read(authProvider.notifier)
|
|
||||||
.setOpenApiServiceEndpoint()
|
|
||||||
.then(logConnectionInfo)
|
|
||||||
.whenComplete(() => resumeSession()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void logConnectionInfo(String? endpoint) {
|
void logConnectionInfo(String? endpoint) {
|
||||||
|
|||||||
+58
-27
@@ -59,9 +59,9 @@ class BackgroundWorkerFgHostApi {
|
|||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<void> enable() async {
|
Future<void> enableSyncWorker() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -82,9 +82,32 @@ class BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disable() async {
|
Future<void> enableUploadWorker(int callbackHandle) async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[callbackHandle]);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disableUploadWorker() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -141,34 +164,13 @@ class BackgroundWorkerBgHostApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
final String pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
|
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
|
||||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
if (pigeonVar_replyList == null) {
|
|
||||||
throw _createConnectionError(pigeonVar_channelName);
|
|
||||||
} else if (pigeonVar_replyList.length > 1) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: pigeonVar_replyList[0]! as String,
|
|
||||||
message: pigeonVar_replyList[1] as String?,
|
|
||||||
details: pigeonVar_replyList[2],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
Future<void> onLocalSync(int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onAndroidUpload();
|
Future<void> onAndroidUpload();
|
||||||
@@ -181,6 +183,35 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
String messageChannelSuffix = '',
|
String messageChannelSuffix = '',
|
||||||
}) {
|
}) {
|
||||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
{
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix',
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: binaryMessenger,
|
||||||
|
);
|
||||||
|
if (api == null) {
|
||||||
|
pigeonVar_channel.setMessageHandler(null);
|
||||||
|
} else {
|
||||||
|
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||||
|
assert(
|
||||||
|
message != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.',
|
||||||
|
);
|
||||||
|
final List<Object?> args = (message as List<Object?>?)!;
|
||||||
|
final int? arg_maxSeconds = (args[0] as int?);
|
||||||
|
try {
|
||||||
|
await api.onLocalSync(arg_maxSeconds);
|
||||||
|
return wrapResponse(empty: true);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
return wrapResponse(error: e);
|
||||||
|
} catch (e) {
|
||||||
|
return wrapResponse(
|
||||||
|
error: PlatformException(code: 'error', message: e.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/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';
|
||||||
@@ -39,14 +38,14 @@ class DriftPlacePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlaceSliverAppBar extends HookWidget {
|
class _PlaceSliverAppBar extends StatelessWidget {
|
||||||
const _PlaceSliverAppBar({required this.search});
|
const _PlaceSliverAppBar({required this.search});
|
||||||
|
|
||||||
final ValueNotifier<String?> search;
|
final ValueNotifier<String?> search;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final searchFocusNode = useFocusNode();
|
final searchFocusNode = FocusNode();
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||||||
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';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.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:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
@@ -40,12 +39,8 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
|||||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||||
bool isGrid = false;
|
bool isGrid = false;
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
|
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||||
final searchFocusNode = FocusNode();
|
final searchFocusNode = FocusNode();
|
||||||
List<RemoteAlbum> sortedAlbums = [];
|
|
||||||
List<RemoteAlbum> shownAlbums = [];
|
|
||||||
|
|
||||||
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
|
|
||||||
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -57,7 +52,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
onSearch(searchController.text, filter.mode);
|
onSearch(searchController.text, filterMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
searchFocusNode.addListener(() {
|
searchFocusNode.addListener(() {
|
||||||
@@ -67,11 +62,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||||
|
|
||||||
filterAlbums();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onRefresh() async {
|
Future<void> onRefresh() async {
|
||||||
@@ -84,60 +77,17 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeFilter(QuickFilterMode mode) {
|
void changeFilter(QuickFilterMode sortMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
filter = filter.copyWith(mode: mode);
|
filterMode = sortMode;
|
||||||
});
|
});
|
||||||
|
|
||||||
filterAlbums();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> changeSort(AlbumSort sort) async {
|
|
||||||
setState(() {
|
|
||||||
this.sort = sort;
|
|
||||||
});
|
|
||||||
|
|
||||||
await sortAlbums();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSearch() {
|
void clearSearch() {
|
||||||
setState(() {
|
setState(() {
|
||||||
filter = filter.copyWith(mode: QuickFilterMode.all, query: null);
|
filterMode = QuickFilterMode.all;
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
});
|
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||||
|
|
||||||
filterAlbums();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> sortAlbums() async {
|
|
||||||
final sorted = await ref
|
|
||||||
.read(remoteAlbumProvider.notifier)
|
|
||||||
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
sortedAlbums = sorted;
|
|
||||||
});
|
|
||||||
|
|
||||||
// we need to re-filter the albums after sorting
|
|
||||||
// so shownAlbums gets updated
|
|
||||||
filterAlbums();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> filterAlbums() async {
|
|
||||||
if (filter.query == null) {
|
|
||||||
setState(() {
|
|
||||||
shownAlbums = sortedAlbums;
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final filteredAlbums = ref
|
|
||||||
.read(remoteAlbumProvider.notifier)
|
|
||||||
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
shownAlbums = filteredAlbums;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +100,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||||
|
|
||||||
// refilter and sort when albums change
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
|
||||||
await sortAlbums();
|
|
||||||
});
|
|
||||||
|
|
||||||
return MultiSliver(
|
return MultiSliver(
|
||||||
children: [
|
children: [
|
||||||
@@ -163,28 +110,26 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
onSearch: onSearch,
|
onSearch: onSearch,
|
||||||
filterMode: filter.mode,
|
filterMode: filterMode,
|
||||||
onClearSearch: clearSearch,
|
onClearSearch: clearSearch,
|
||||||
),
|
),
|
||||||
_QuickFilterButtonRow(
|
_QuickFilterButtonRow(
|
||||||
filterMode: filter.mode,
|
filterMode: filterMode,
|
||||||
onChangeFilter: changeFilter,
|
onChangeFilter: changeFilter,
|
||||||
onSearch: onSearch,
|
onSearch: onSearch,
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
),
|
),
|
||||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
|
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
|
||||||
isGrid
|
isGrid
|
||||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SortButton extends ConsumerStatefulWidget {
|
class _SortButton extends ConsumerStatefulWidget {
|
||||||
const _SortButton(this.onSortChanged);
|
const _SortButton();
|
||||||
|
|
||||||
final Future<void> Function(AlbumSort) onSortChanged;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||||
@@ -203,15 +148,15 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
|||||||
albumSortIsReverse = !albumSortIsReverse;
|
albumSortIsReverse = !albumSortIsReverse;
|
||||||
isSorting = true;
|
isSorting = true;
|
||||||
});
|
});
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
albumSortOption = sortMode;
|
albumSortOption = sortMode;
|
||||||
isSorting = true;
|
isSorting = true;
|
||||||
});
|
});
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse));
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isSorting = false;
|
isSorting = false;
|
||||||
});
|
});
|
||||||
@@ -449,11 +394,10 @@ class _QuickFilterButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QuickSortAndViewMode extends StatelessWidget {
|
class _QuickSortAndViewMode extends StatelessWidget {
|
||||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});
|
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
|
||||||
|
|
||||||
final bool isGrid;
|
final bool isGrid;
|
||||||
final VoidCallback onToggleViewMode;
|
final VoidCallback onToggleViewMode;
|
||||||
final Future<void> Function(AlbumSort) onSortChanged;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -463,7 +407,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_SortButton(onSortChanged),
|
const _SortButton(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||||
onPressed: onToggleViewMode,
|
onPressed: onToggleViewMode,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
@@ -130,7 +129,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
reloadSubscription?.cancel();
|
reloadSubscription?.cancel();
|
||||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +596,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
// Rebuild the widget when the asset viewer state changes
|
// Rebuild the widget when the asset viewer state changes
|
||||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
|
||||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||||
ref.watch(isPlayingMotionVideoProvider);
|
ref.watch(isPlayingMotionVideoProvider);
|
||||||
@@ -615,15 +612,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for control visibility changes and change system UI mode accordingly
|
|
||||||
ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async {
|
|
||||||
if (showingControls) {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
|
||||||
} else {
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
duration: Durations.short2,
|
duration: Durations.short2,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: Durations.short4,
|
duration: Durations.short4,
|
||||||
child: isSheetOpen
|
child: isSheetOpen || isReadonlyModeEnabled
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Theme(
|
: Theme(
|
||||||
data: context.themeData.copyWith(
|
data: context.themeData.copyWith(
|
||||||
@@ -72,14 +72,14 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
height: context.padding.bottom + (asset.isVideo ? 160 : 90),
|
||||||
color: Colors.black.withAlpha(125),
|
color: Colors.black.withAlpha(125),
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16),
|
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (asset.isVideo) const VideoControls(),
|
if (asset.isVideo) const VideoControls(),
|
||||||
if (!isInLockedView && !isReadonlyModeEnabled)
|
if (!isInLockedView) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
@@ -18,74 +16,22 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||||
final RemoteAlbum album;
|
final RemoteAlbum album;
|
||||||
const RemoteAlbumBottomSheet({super.key, required this.album});
|
const RemoteAlbumBottomSheet({super.key, required this.album});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<RemoteAlbumBottomSheet> createState() => _RemoteAlbumBottomSheetState();
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
}
|
|
||||||
|
|
||||||
class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet> {
|
|
||||||
late DraggableScrollableController sheetController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
sheetController = DraggableScrollableController();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
sheetController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final multiselect = ref.watch(multiSelectProvider);
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
|
||||||
final selectedAssets = multiselect.selectedAssets;
|
|
||||||
if (selectedAssets.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final addedCount = await ref
|
|
||||||
.read(remoteAlbumProvider.notifier)
|
|
||||||
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
|
||||||
|
|
||||||
if (addedCount != selectedAssets.length) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onKeyboardExpand() {
|
|
||||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
controller: sheetController,
|
initialChildSize: 0.25,
|
||||||
initialChildSize: 0.45,
|
maxChildSize: 0.4,
|
||||||
maxChildSize: 0.85,
|
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
@@ -106,11 +52,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
|
||||||
],
|
|
||||||
slivers: [
|
|
||||||
const AddToAlbumHeader(),
|
|
||||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
yield image;
|
yield image;
|
||||||
|
} catch (e) {
|
||||||
|
evict();
|
||||||
|
if (!isCancelled) {
|
||||||
|
_log.severe('Error loading image', e);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.request = null;
|
this.request = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
|
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||||
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
||||||
static final cacheManager = RemoteThumbnailCacheManager();
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
|
||||||
RemoteThumbProvider({required this.assetId});
|
RemoteThumbProvider({required this.assetId});
|
||||||
@@ -39,7 +37,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
|||||||
final request = this.request = RemoteImageRequest(
|
final request = this.request = RemoteImageRequest(
|
||||||
uri: getThumbnailUrlForRemoteId(key.assetId),
|
uri: getThumbnailUrlForRemoteId(key.assetId),
|
||||||
headers: ApiService.getRequestHeaders(),
|
headers: ApiService.getRequestHeaders(),
|
||||||
cacheManager: cacheManager,
|
|
||||||
);
|
);
|
||||||
return loadRequest(request, decode);
|
return loadRequest(request, decode);
|
||||||
}
|
}
|
||||||
@@ -60,7 +57,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
|||||||
|
|
||||||
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
||||||
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
||||||
static final cacheManager = RemoteThumbnailCacheManager();
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
|
||||||
RemoteFullImageProvider({required this.assetId});
|
RemoteFullImageProvider({required this.assetId});
|
||||||
@@ -92,11 +88,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
|||||||
}
|
}
|
||||||
|
|
||||||
final headers = ApiService.getRequestHeaders();
|
final headers = ApiService.getRequestHeaders();
|
||||||
final request = this.request = RemoteImageRequest(
|
final request = this.request = RemoteImageRequest(uri: getPreviewUrlForRemoteId(key.assetId), headers: headers);
|
||||||
uri: getPreviewUrlForRemoteId(key.assetId),
|
|
||||||
headers: headers,
|
|
||||||
cacheManager: cacheManager,
|
|
||||||
);
|
|
||||||
yield* loadRequest(request, decode);
|
yield* loadRequest(request, decode);
|
||||||
|
|
||||||
if (isCancelled) {
|
if (isCancelled) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import 'dart:ui';
|
|||||||
|
|
||||||
const double kTimelineHeaderExtent = 80.0;
|
const double kTimelineHeaderExtent = 80.0;
|
||||||
const Size kTimelineFixedTileExtent = Size.square(256);
|
const Size kTimelineFixedTileExtent = Size.square(256);
|
||||||
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
|
|
||||||
const double kTimelineSpacing = 2.0;
|
const double kTimelineSpacing = 2.0;
|
||||||
const int kTimelineColumnCount = 3;
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
|
const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
|
||||||
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
|
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
|
||||||
|
|
||||||
|
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
|
||||||
|
const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
import 'package:intl/intl.dart' hide TextDirection;
|
import 'package:intl/intl.dart' hide TextDirection;
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
|
||||||
|
|
||||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||||
/// for quick navigation of the BoxScrollView.
|
/// for quick navigation of the BoxScrollView.
|
||||||
@@ -75,7 +74,6 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
||||||
String? _lastLabel;
|
|
||||||
double _thumbTopOffset = 0.0;
|
double _thumbTopOffset = 0.0;
|
||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
List<_Segment> _segments = [];
|
List<_Segment> _segments = [];
|
||||||
@@ -174,7 +172,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
_isDragging = true;
|
_isDragging = true;
|
||||||
_labelAnimationController.forward();
|
_labelAnimationController.forward();
|
||||||
_fadeOutTimer?.cancel();
|
_fadeOutTimer?.cancel();
|
||||||
_lastLabel = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +189,6 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
|
|
||||||
if (nearestMonthSegment != null) {
|
if (nearestMonthSegment != null) {
|
||||||
_snapToSegment(nearestMonthSegment);
|
_snapToSegment(nearestMonthSegment);
|
||||||
final label = nearestMonthSegment.scrollLabel;
|
|
||||||
if (_lastLabel != label) {
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
|
||||||
_lastLabel = label;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
@@ -82,12 +81,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
|
||||||
|
|
||||||
lockManager.requestHolderToClose();
|
|
||||||
debugPrint("Requested lock holder to close on resume");
|
|
||||||
await lockManager.acquireLock();
|
|
||||||
debugPrint("Lock acquired for background sync on resume");
|
|
||||||
|
|
||||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||||
// Ensure proper cleanup before starting new background tasks
|
// Ensure proper cleanup before starting new background tasks
|
||||||
@@ -137,7 +130,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
// do not stop/clean up anything on inactivity: issued on every orientation change
|
// do not stop/clean up anything on inactivity: issued on every orientation change
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleAppPause() async {
|
void handleAppPause() {
|
||||||
state = AppLifeCycleEnum.paused;
|
state = AppLifeCycleEnum.paused;
|
||||||
_wasPaused = true;
|
_wasPaused = true;
|
||||||
|
|
||||||
@@ -147,12 +140,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||||
_ref.read(backupProvider.notifier).cancelBackup();
|
_ref.read(backupProvider.notifier).cancelBackup();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
|
||||||
await backgroundManager.cancel();
|
|
||||||
await backgroundManager.cancelLocal();
|
|
||||||
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
|
||||||
debugPrint("Lock released on app pause");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ref.read(websocketProvider.notifier).disconnect();
|
_ref.read(websocketProvider.notifier).disconnect();
|
||||||
@@ -186,7 +173,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
|
||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
|
|
||||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||||
@@ -19,7 +18,3 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
|||||||
ref.onDispose(manager.cancel);
|
ref.onDispose(manager.cancel);
|
||||||
return manager;
|
return manager;
|
||||||
});
|
});
|
||||||
|
|
||||||
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
|
|
||||||
return IsolateLockManager(portName: name);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,148 +1,25 @@
|
|||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
// ignore: implementation_imports
|
|
||||||
import 'package:flutter_cache_manager/src/cache_store.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
abstract class RemoteCacheManager extends CacheManager {
|
class RemoteImageCacheManager extends CacheManager {
|
||||||
static final _log = Logger('RemoteCacheManager');
|
|
||||||
|
|
||||||
RemoteCacheManager.custom(super.config, CacheStore store)
|
|
||||||
// Unfortunately, CacheStore is not a public API
|
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
|
||||||
: super.custom(cacheStore: store);
|
|
||||||
|
|
||||||
Future<void> putStreamedFile(
|
|
||||||
String url,
|
|
||||||
Stream<List<int>> source, {
|
|
||||||
String? key,
|
|
||||||
String? eTag,
|
|
||||||
Duration maxAge = const Duration(days: 30),
|
|
||||||
String fileExtension = 'file',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unlike `putFileStream`, this method handles request cancellation,
|
|
||||||
// does not make a (slow) DB call checking if the file is already cached,
|
|
||||||
// does not synchronously check if a file exists,
|
|
||||||
// and deletes the file on cancellation without making these checks again.
|
|
||||||
Future<void> putStreamedFileToStore(
|
|
||||||
CacheStore store,
|
|
||||||
String url,
|
|
||||||
Stream<List<int>> source, {
|
|
||||||
String? key,
|
|
||||||
String? eTag,
|
|
||||||
Duration maxAge = const Duration(days: 30),
|
|
||||||
String fileExtension = 'file',
|
|
||||||
}) async {
|
|
||||||
final path = '${const Uuid().v1()}.$fileExtension';
|
|
||||||
final file = await store.fileSystem.createFile(path);
|
|
||||||
final sink = file.openWrite();
|
|
||||||
try {
|
|
||||||
await source.listen(sink.add, cancelOnError: true).asFuture();
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
await sink.close();
|
|
||||||
await file.delete();
|
|
||||||
} catch (e) {
|
|
||||||
_log.severe('Failed to delete incomplete cache file: $e');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sink.flush();
|
|
||||||
await sink.close();
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
await file.delete();
|
|
||||||
} catch (e) {
|
|
||||||
_log.severe('Failed to delete incomplete cache file: $e');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final cacheObject = CacheObject(
|
|
||||||
url,
|
|
||||||
key: key,
|
|
||||||
relativePath: path,
|
|
||||||
validTill: DateTime.now().add(maxAge),
|
|
||||||
eTag: eTag,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await store.putFile(cacheObject);
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
await file.delete();
|
|
||||||
} catch (e) {
|
|
||||||
_log.severe('Failed to delete untracked cache file: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteImageCacheManager extends RemoteCacheManager {
|
|
||||||
static const key = 'remoteImageCacheKey';
|
static const key = 'remoteImageCacheKey';
|
||||||
static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
|
static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
|
||||||
static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30));
|
static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30));
|
||||||
static final _store = CacheStore(_config);
|
|
||||||
|
|
||||||
factory RemoteImageCacheManager() {
|
factory RemoteImageCacheManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteImageCacheManager._() : super.custom(_config, _store);
|
RemoteImageCacheManager._() : super(_config);
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> putStreamedFile(
|
|
||||||
String url,
|
|
||||||
Stream<List<int>> source, {
|
|
||||||
String? key,
|
|
||||||
String? eTag,
|
|
||||||
Duration maxAge = const Duration(days: 30),
|
|
||||||
String fileExtension = 'file',
|
|
||||||
}) {
|
|
||||||
return putStreamedFileToStore(
|
|
||||||
_store,
|
|
||||||
url,
|
|
||||||
source,
|
|
||||||
key: key,
|
|
||||||
eTag: eTag,
|
|
||||||
maxAge: maxAge,
|
|
||||||
fileExtension: fileExtension,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The cache manager for full size images [ImmichRemoteImageProvider]
|
class RemoteThumbnailCacheManager extends CacheManager {
|
||||||
class RemoteThumbnailCacheManager extends RemoteCacheManager {
|
|
||||||
static const key = 'remoteThumbnailCacheKey';
|
static const key = 'remoteThumbnailCacheKey';
|
||||||
static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._();
|
static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._();
|
||||||
static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30));
|
static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30));
|
||||||
static final _store = CacheStore(_config);
|
|
||||||
|
|
||||||
factory RemoteThumbnailCacheManager() {
|
factory RemoteThumbnailCacheManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteThumbnailCacheManager._() : super.custom(_config, _store);
|
RemoteThumbnailCacheManager._() : super(_config);
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> putStreamedFile(
|
|
||||||
String url,
|
|
||||||
Stream<List<int>> source, {
|
|
||||||
String? key,
|
|
||||||
String? eTag,
|
|
||||||
Duration maxAge = const Duration(days: 30),
|
|
||||||
String fileExtension = 'file',
|
|
||||||
}) {
|
|
||||||
return putStreamedFileToStore(
|
|
||||||
_store,
|
|
||||||
url,
|
|
||||||
source,
|
|
||||||
key: key,
|
|
||||||
eTag: eTag,
|
|
||||||
maxAge: maxAge,
|
|
||||||
fileExtension: fileExtension,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,42 +12,43 @@ import 'album.provider.dart';
|
|||||||
|
|
||||||
class RemoteAlbumState {
|
class RemoteAlbumState {
|
||||||
final List<RemoteAlbum> albums;
|
final List<RemoteAlbum> albums;
|
||||||
|
final List<RemoteAlbum> filteredAlbums;
|
||||||
|
|
||||||
const RemoteAlbumState({required this.albums});
|
const RemoteAlbumState({required this.albums, List<RemoteAlbum>? filteredAlbums})
|
||||||
|
: filteredAlbums = filteredAlbums ?? albums;
|
||||||
|
|
||||||
RemoteAlbumState copyWith({List<RemoteAlbum>? albums}) {
|
RemoteAlbumState copyWith({List<RemoteAlbum>? albums, List<RemoteAlbum>? filteredAlbums}) {
|
||||||
return RemoteAlbumState(albums: albums ?? this.albums);
|
return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'RemoteAlbumState(albums: ${albums.length})';
|
String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant RemoteAlbumState other) {
|
bool operator ==(covariant RemoteAlbumState other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
return listEquals(other.albums, albums);
|
return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => albums.hashCode;
|
int get hashCode => albums.hashCode ^ filteredAlbums.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
late RemoteAlbumService _remoteAlbumService;
|
late RemoteAlbumService _remoteAlbumService;
|
||||||
final _logger = Logger('RemoteAlbumNotifier');
|
final _logger = Logger('RemoteAlbumNotifier');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RemoteAlbumState build() {
|
RemoteAlbumState build() {
|
||||||
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
||||||
return const RemoteAlbumState(albums: []);
|
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _getAll() async {
|
Future<List<RemoteAlbum>> _getAll() async {
|
||||||
try {
|
try {
|
||||||
final albums = await _remoteAlbumService.getAll();
|
final albums = await _remoteAlbumService.getAll();
|
||||||
state = state.copyWith(albums: albums);
|
state = state.copyWith(albums: albums, filteredAlbums: albums);
|
||||||
return albums;
|
return albums;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to fetch albums', error, stack);
|
_logger.severe('Failed to fetch albums', error, stack);
|
||||||
@@ -59,21 +60,19 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
await _getAll();
|
await _getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RemoteAlbum> searchAlbums(
|
void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) {
|
||||||
List<RemoteAlbum> albums,
|
final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode);
|
||||||
String query,
|
|
||||||
String? userId, [
|
state = state.copyWith(filteredAlbums: filtered);
|
||||||
QuickFilterMode filterMode = QuickFilterMode.all,
|
|
||||||
]) {
|
|
||||||
return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> sortAlbums(
|
void clearSearch() {
|
||||||
List<RemoteAlbum> albums,
|
state = state.copyWith(filteredAlbums: state.albums);
|
||||||
RemoteAlbumSortMode sortMode, {
|
}
|
||||||
bool isReverse = false,
|
|
||||||
}) async {
|
Future<void> sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async {
|
||||||
return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse);
|
final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||||
|
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RemoteAlbum?> createAlbum({
|
Future<RemoteAlbum?> createAlbum({
|
||||||
@@ -84,7 +83,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
try {
|
try {
|
||||||
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
||||||
|
|
||||||
state = state.copyWith(albums: [...state.albums, album]);
|
state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]);
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
@@ -115,7 +114,11 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
return album.id == albumId ? updatedAlbum : album;
|
return album.id == albumId ? updatedAlbum : album;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
state = state.copyWith(albums: updatedAlbums);
|
final updatedFilteredAlbums = state.filteredAlbums.map((album) {
|
||||||
|
return album.id == albumId ? updatedAlbum : album;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||||
|
|
||||||
return updatedAlbum;
|
return updatedAlbum;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
@@ -136,7 +139,9 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
await _remoteAlbumService.deleteAlbum(albumId);
|
await _remoteAlbumService.deleteAlbum(albumId);
|
||||||
|
|
||||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
state = state.copyWith(albums: updatedAlbums);
|
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||||
|
|
||||||
|
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
@@ -159,7 +164,9 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
state = state.copyWith(albums: updatedAlbums);
|
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||||
|
|
||||||
|
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setActivityStatus(String albumId, bool enabled) {
|
Future<void> setActivityStatus(String albumId, bool enabled) {
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/utils/user_agent.dart';
|
import 'package:immich_mobile/utils/user_agent.dart';
|
||||||
|
|
||||||
class ApiService implements Authentication {
|
class ApiService implements Authentication {
|
||||||
|
static final _client = const NetworkRepository().getHttpClient('api');
|
||||||
late ApiClient _apiClient;
|
late ApiClient _apiClient;
|
||||||
|
|
||||||
late UsersApi usersApi;
|
late UsersApi usersApi;
|
||||||
@@ -50,6 +51,7 @@ class ApiService implements Authentication {
|
|||||||
|
|
||||||
setEndpoint(String endpoint) {
|
setEndpoint(String endpoint) {
|
||||||
_apiClient = ApiClient(basePath: endpoint, authentication: this);
|
_apiClient = ApiClient(basePath: endpoint, authentication: this);
|
||||||
|
_apiClient.client = _client;
|
||||||
_setUserAgentHeader();
|
_setUserAgentHeader();
|
||||||
if (_accessToken != null) {
|
if (_accessToken != null) {
|
||||||
setAccessToken(_accessToken!);
|
setAccessToken(_accessToken!);
|
||||||
@@ -134,13 +136,11 @@ class ApiService implements Authentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getWellKnownEndpoint(String baseUrl) async {
|
Future<String> _getWellKnownEndpoint(String baseUrl) async {
|
||||||
final Client client = Client();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var headers = {"Accept": "application/json"};
|
var headers = {"Accept": "application/json"};
|
||||||
headers.addAll(getRequestHeaders());
|
headers.addAll(getRequestHeaders());
|
||||||
|
|
||||||
final res = await client
|
final res = await _client
|
||||||
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
|
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
|
||||||
|
|
||||||
class AlbumFilter {
|
|
||||||
String? userId;
|
|
||||||
String? query;
|
|
||||||
QuickFilterMode mode;
|
|
||||||
|
|
||||||
AlbumFilter({required this.mode, this.userId, this.query});
|
|
||||||
|
|
||||||
AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) {
|
|
||||||
return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumSort {
|
|
||||||
RemoteAlbumSortMode mode;
|
|
||||||
bool isReverse;
|
|
||||||
|
|
||||||
AlbumSort({required this.mode, this.isReverse = false});
|
|
||||||
|
|
||||||
AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) {
|
|
||||||
return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const int _maxMillisecondsSinceEpoch = 8640000000000000; // 275760-09-13
|
|
||||||
const int _minMillisecondsSinceEpoch = -62135596800000; // 0001-01-01
|
|
||||||
|
|
||||||
DateTime? tryFromSecondsSinceEpoch(int? secondsSinceEpoch) {
|
|
||||||
if (secondsSinceEpoch == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final milliSeconds = secondsSinceEpoch * 1000;
|
|
||||||
if (milliSeconds < _minMillisecondsSinceEpoch || milliSeconds > _maxMillisecondsSinceEpoch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return DateTime.fromMillisecondsSinceEpoch(milliSeconds);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,14 +8,12 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
|
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/drag_sheet.dart';
|
import 'package:immich_mobile/widgets/common/drag_sheet.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
||||||
|
|
||||||
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
||||||
@@ -47,7 +45,6 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
final bool unfavorite;
|
final bool unfavorite;
|
||||||
final bool unarchive;
|
final bool unarchive;
|
||||||
final AssetSelectionState selectionAssetState;
|
final AssetSelectionState selectionAssetState;
|
||||||
final List<Asset> selectedAssets;
|
|
||||||
|
|
||||||
const ControlBottomAppBar({
|
const ControlBottomAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -67,7 +64,6 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
this.onRemoveFromAlbum,
|
this.onRemoveFromAlbum,
|
||||||
this.onToggleLocked,
|
this.onToggleLocked,
|
||||||
this.selectionAssetState = const AssetSelectionState(),
|
this.selectionAssetState = const AssetSelectionState(),
|
||||||
this.selectedAssets = const [],
|
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.unarchive = false,
|
this.unarchive = false,
|
||||||
this.unfavorite = false,
|
this.unfavorite = false,
|
||||||
@@ -104,18 +100,6 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show existing AddToAlbumBottomSheet
|
|
||||||
void showAddToAlbumBottomSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
elevation: 0,
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext _) {
|
|
||||||
return AddToAlbumBottomSheet(assets: selectedAssets);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) {
|
void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
deleteCb(force);
|
deleteCb(force);
|
||||||
@@ -137,15 +121,6 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
label: "share_link".tr(),
|
label: "share_link".tr(),
|
||||||
onPressed: enabled ? () => onShare(false) : null,
|
onPressed: enabled ? () => onShare(false) : null,
|
||||||
),
|
),
|
||||||
if (!isInLockedView && hasRemote && albums.isNotEmpty)
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 100),
|
|
||||||
child: ControlBoxButton(
|
|
||||||
iconData: Icons.photo_album,
|
|
||||||
label: "add_to_album".tr(),
|
|
||||||
onPressed: enabled ? showAddToAlbumBottomSheet : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (hasRemote && onArchive != null)
|
if (hasRemote && onArchive != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined,
|
iconData: unarchive ? Icons.unarchive_outlined : Icons.archive_outlined,
|
||||||
|
|||||||
@@ -440,7 +440,6 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
onUpload: onUpload,
|
onUpload: onUpload,
|
||||||
enabled: !processing.value,
|
enabled: !processing.value,
|
||||||
selectionAssetState: selectionAssetState.value,
|
selectionAssetState: selectionAssetState.value,
|
||||||
selectedAssets: selection.value.toList(),
|
|
||||||
onStack: stackEnabled ? onStack : null,
|
onStack: stackEnabled ? onStack : null,
|
||||||
onEditTime: editEnabled ? onEditTime : null,
|
onEditTime: editEnabled ? onEditTime : null,
|
||||||
onEditLocation: editEnabled ? onEditLocation : null,
|
onEditLocation: editEnabled ? onEditLocation : null,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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:immich_mobile/entities/store.entity.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/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
@@ -260,7 +259,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||||||
const AppBarProfileInfoBox(),
|
const AppBarProfileInfoBox(),
|
||||||
buildStorageInformation(),
|
buildStorageInformation(),
|
||||||
const AppBarServerInfo(),
|
const AppBarServerInfo(),
|
||||||
if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(),
|
if (isReadonlyModeEnabled) buildReadonlyMessage(),
|
||||||
buildAppLogButton(),
|
buildAppLogButton(),
|
||||||
buildSettingButton(),
|
buildSettingButton(),
|
||||||
buildSignOutButton(),
|
buildSignOutButton(),
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class PhotoViewCore extends StatefulWidget {
|
|||||||
|
|
||||||
class PhotoViewCoreState extends State<PhotoViewCore>
|
class PhotoViewCoreState extends State<PhotoViewCore>
|
||||||
with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector {
|
with TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector {
|
||||||
|
Offset? _normalizedPosition;
|
||||||
double? _scaleBefore;
|
double? _scaleBefore;
|
||||||
double? _rotationBefore;
|
double? _rotationBefore;
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
void onScaleStart(ScaleStartDetails details) {
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
_rotationBefore = controller.rotation;
|
_rotationBefore = controller.rotation;
|
||||||
_scaleBefore = scale;
|
_scaleBefore = scale;
|
||||||
|
_normalizedPosition = details.focalPoint - controller.position;
|
||||||
_scaleAnimationController.stop();
|
_scaleAnimationController.stop();
|
||||||
_positionAnimationController.stop();
|
_positionAnimationController.stop();
|
||||||
_rotationAnimationController.stop();
|
_rotationAnimationController.stop();
|
||||||
@@ -164,14 +166,8 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
};
|
};
|
||||||
|
|
||||||
void onScaleUpdate(ScaleUpdateDetails details) {
|
void onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
final centeredFocalPoint = Offset(
|
|
||||||
details.focalPoint.dx - scaleBoundaries.outerSize.width / 2,
|
|
||||||
details.focalPoint.dy - scaleBoundaries.outerSize.height / 2,
|
|
||||||
);
|
|
||||||
final double newScale = _scaleBefore! * details.scale;
|
final double newScale = _scaleBefore! * details.scale;
|
||||||
final double scaleDelta = newScale / scale;
|
Offset delta = details.focalPoint - _normalizedPosition!;
|
||||||
final Offset newPosition =
|
|
||||||
(controller.position + details.focalPointDelta) * scaleDelta - centeredFocalPoint * (scaleDelta - 1);
|
|
||||||
|
|
||||||
updateScaleStateFromNewScale(newScale);
|
updateScaleStateFromNewScale(newScale);
|
||||||
|
|
||||||
@@ -180,7 +176,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
|
|
||||||
updateMultiple(
|
updateMultiple(
|
||||||
scale: newScale,
|
scale: newScale,
|
||||||
position: panEnabled ? newPosition : clampPosition(position: newPosition),
|
position: panEnabled ? delta : clampPosition(position: delta * details.scale),
|
||||||
rotation: rotationEnabled ? _rotationBefore! + details.rotation : null,
|
rotation: rotationEnabled ? _rotationBefore! + details.rotation : null,
|
||||||
rotationFocusPoint: rotationEnabled ? details.focalPoint : null,
|
rotationFocusPoint: rotationEnabled ? details.focalPoint : null,
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+2
-2
@@ -18,7 +18,7 @@ class AssetsApi {
|
|||||||
|
|
||||||
/// checkBulkUpload
|
/// checkBulkUpload
|
||||||
///
|
///
|
||||||
/// Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission.
|
/// Checks if assets exist by checksums
|
||||||
///
|
///
|
||||||
/// Note: This method returns the HTTP [Response].
|
/// Note: This method returns the HTTP [Response].
|
||||||
///
|
///
|
||||||
@@ -52,7 +52,7 @@ class AssetsApi {
|
|||||||
|
|
||||||
/// checkBulkUpload
|
/// checkBulkUpload
|
||||||
///
|
///
|
||||||
/// Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission.
|
/// Checks if assets exist by checksums
|
||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
+6
-32
@@ -31,8 +31,7 @@ class SmartSearchDto {
|
|||||||
this.model,
|
this.model,
|
||||||
this.page,
|
this.page,
|
||||||
this.personIds = const [],
|
this.personIds = const [],
|
||||||
this.query,
|
required this.query,
|
||||||
this.queryAssetId,
|
|
||||||
this.rating,
|
this.rating,
|
||||||
this.size,
|
this.size,
|
||||||
this.state,
|
this.state,
|
||||||
@@ -152,21 +151,7 @@ class SmartSearchDto {
|
|||||||
|
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
|
|
||||||
///
|
String query;
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? query;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? queryAssetId;
|
|
||||||
|
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
@@ -293,7 +278,6 @@ class SmartSearchDto {
|
|||||||
other.page == page &&
|
other.page == page &&
|
||||||
_deepEquality.equals(other.personIds, personIds) &&
|
_deepEquality.equals(other.personIds, personIds) &&
|
||||||
other.query == query &&
|
other.query == query &&
|
||||||
other.queryAssetId == queryAssetId &&
|
|
||||||
other.rating == rating &&
|
other.rating == rating &&
|
||||||
other.size == size &&
|
other.size == size &&
|
||||||
other.state == state &&
|
other.state == state &&
|
||||||
@@ -330,8 +314,7 @@ class SmartSearchDto {
|
|||||||
(model == null ? 0 : model!.hashCode) +
|
(model == null ? 0 : model!.hashCode) +
|
||||||
(page == null ? 0 : page!.hashCode) +
|
(page == null ? 0 : page!.hashCode) +
|
||||||
(personIds.hashCode) +
|
(personIds.hashCode) +
|
||||||
(query == null ? 0 : query!.hashCode) +
|
(query.hashCode) +
|
||||||
(queryAssetId == null ? 0 : queryAssetId!.hashCode) +
|
|
||||||
(rating == null ? 0 : rating!.hashCode) +
|
(rating == null ? 0 : rating!.hashCode) +
|
||||||
(size == null ? 0 : size!.hashCode) +
|
(size == null ? 0 : size!.hashCode) +
|
||||||
(state == null ? 0 : state!.hashCode) +
|
(state == null ? 0 : state!.hashCode) +
|
||||||
@@ -348,7 +331,7 @@ class SmartSearchDto {
|
|||||||
(withExif == null ? 0 : withExif!.hashCode);
|
(withExif == null ? 0 : withExif!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
|
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -434,16 +417,7 @@ class SmartSearchDto {
|
|||||||
// json[r'page'] = null;
|
// json[r'page'] = null;
|
||||||
}
|
}
|
||||||
json[r'personIds'] = this.personIds;
|
json[r'personIds'] = this.personIds;
|
||||||
if (this.query != null) {
|
|
||||||
json[r'query'] = this.query;
|
json[r'query'] = this.query;
|
||||||
} else {
|
|
||||||
// json[r'query'] = null;
|
|
||||||
}
|
|
||||||
if (this.queryAssetId != null) {
|
|
||||||
json[r'queryAssetId'] = this.queryAssetId;
|
|
||||||
} else {
|
|
||||||
// json[r'queryAssetId'] = null;
|
|
||||||
}
|
|
||||||
if (this.rating != null) {
|
if (this.rating != null) {
|
||||||
json[r'rating'] = this.rating;
|
json[r'rating'] = this.rating;
|
||||||
} else {
|
} else {
|
||||||
@@ -548,8 +522,7 @@ class SmartSearchDto {
|
|||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
query: mapValueOfType<String>(json, r'query'),
|
query: mapValueOfType<String>(json, r'query')!,
|
||||||
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
@@ -613,6 +586,7 @@ class SmartSearchDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
|
'query',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-3
@@ -69,7 +69,6 @@ class SyncEntityType {
|
|||||||
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
|
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
||||||
static const syncCompleteV1 = SyncEntityType._(r'SyncCompleteV1');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
static const values = <SyncEntityType>[
|
static const values = <SyncEntityType>[
|
||||||
@@ -119,7 +118,6 @@ class SyncEntityType {
|
|||||||
userMetadataDeleteV1,
|
userMetadataDeleteV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
syncResetV1,
|
syncResetV1,
|
||||||
syncCompleteV1,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||||
@@ -204,7 +202,6 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
|
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
||||||
case r'SyncCompleteV1': return SyncEntityType.syncCompleteV1;
|
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ import 'package:pigeon/pigeon.dart';
|
|||||||
)
|
)
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class BackgroundWorkerFgHostApi {
|
abstract class BackgroundWorkerFgHostApi {
|
||||||
void enable();
|
void enableSyncWorker();
|
||||||
|
|
||||||
void disable();
|
// Enables the background upload service with the given callback handle
|
||||||
|
void enableUploadWorker(int callbackHandle);
|
||||||
|
|
||||||
|
// Disables the background upload service
|
||||||
|
void disableUploadWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
@@ -23,13 +27,14 @@ abstract class BackgroundWorkerBgHostApi {
|
|||||||
// Called from the background flutter engine when it has bootstrapped and established the
|
// Called from the background flutter engine when it has bootstrapped and established the
|
||||||
// required platform channels to notify the native side to start the background upload
|
// required platform channels to notify the native side to start the background upload
|
||||||
void onInitialized();
|
void onInitialized();
|
||||||
|
|
||||||
// Called from the background flutter engine to request the native side to cleanup
|
|
||||||
void close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FlutterApi()
|
@FlutterApi()
|
||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
|
// Android & iOS: Called when the local sync is triggered
|
||||||
|
@async
|
||||||
|
void onLocalSync(int? maxSeconds);
|
||||||
|
|
||||||
// iOS Only: Called when the iOS background upload is triggered
|
// iOS Only: Called when the iOS background upload is triggered
|
||||||
@async
|
@async
|
||||||
void onIosUpload(bool isRefresh, int? maxSeconds);
|
void onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|||||||
+42
-2
@@ -337,6 +337,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cronet_http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cronet_http
|
||||||
|
sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
crop_image:
|
crop_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -369,6 +377,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
cupertino_http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_http
|
||||||
|
sha256: "72187f715837290a63479a5b0ae709f4fedad0ed6bd0441c275eceaa02d5abae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
custom_lint:
|
custom_lint:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -899,10 +915,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.5.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -919,6 +935,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
http_profile:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_profile
|
||||||
|
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1044,6 +1068,14 @@ packages:
|
|||||||
url: "https://github.com/immich-app/isar"
|
url: "https://github.com/immich-app/isar"
|
||||||
source: git
|
source: git
|
||||||
version: "3.1.8"
|
version: "3.1.8"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.2"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1237,6 +1269,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.0"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ dependencies:
|
|||||||
# DB
|
# DB
|
||||||
drift: ^2.23.1
|
drift: ^2.23.1
|
||||||
drift_flutter: ^0.2.4
|
drift_flutter: ^0.2.4
|
||||||
|
cronet_http: ^1.5.0
|
||||||
|
cupertino_http: ^2.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('tryFromSecondsSinceEpoch', () {
|
|
||||||
test('returns null for null input', () {
|
|
||||||
final result = tryFromSecondsSinceEpoch(null);
|
|
||||||
expect(result, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns null for value below minimum allowed range', () {
|
|
||||||
// _minMillisecondsSinceEpoch = -62135596800000
|
|
||||||
final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns null for value above maximum allowed range', () {
|
|
||||||
// _maxMillisecondsSinceEpoch = 8640000000000000
|
|
||||||
final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct DateTime for minimum allowed value', () {
|
|
||||||
final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct DateTime for maximum allowed value', () {
|
|
||||||
final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct DateTime for negative timestamp', () {
|
|
||||||
final seconds = -1577836800; // Dec 31, 1919 (pre-epoch)
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct DateTime for zero timestamp', () {
|
|
||||||
final seconds = 0; // Jan 1, 1970 (epoch)
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result, DateTime.fromMillisecondsSinceEpoch(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct DateTime for recent timestamp', () {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final seconds = now.millisecondsSinceEpoch ~/ 1000;
|
|
||||||
final result = tryFromSecondsSinceEpoch(seconds);
|
|
||||||
expect(result?.year, now.year);
|
|
||||||
expect(result?.month, now.month);
|
|
||||||
expect(result?.day, now.day);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ function dart {
|
|||||||
patch --no-backup-if-mismatch -u api.mustache <api.mustache.patch
|
patch --no-backup-if-mismatch -u api.mustache <api.mustache.patch
|
||||||
|
|
||||||
cd ../../
|
cd ../../
|
||||||
pnpm dlx @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
|
pnpx @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
|
||||||
|
|
||||||
# Post generate patches
|
# Post generate patches
|
||||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
|
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
|
||||||
@@ -27,7 +27,7 @@ function dart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function typescript {
|
function typescript {
|
||||||
pnpm dlx oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
pnpx oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
||||||
pnpm --filter @immich/sdk install --frozen-lockfile
|
pnpm --filter @immich/sdk install --frozen-lockfile
|
||||||
pnpm --filter @immich/sdk build
|
pnpm --filter @immich/sdk build
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,8 @@ function typescript {
|
|||||||
# requires server to be built
|
# requires server to be built
|
||||||
(
|
(
|
||||||
cd ..
|
cd ..
|
||||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true mise run server:build
|
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich build
|
||||||
mise run server:open-api
|
pnpm --filter immich sync:open-api
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ $1 == 'dart' ]]; then
|
if [[ $1 == 'dart' ]]; then
|
||||||
|
|||||||
@@ -1855,7 +1855,7 @@
|
|||||||
},
|
},
|
||||||
"/assets/bulk-upload-check": {
|
"/assets/bulk-upload-check": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Checks if assets exist by checksums. This endpoint requires the `asset.upload` permission.",
|
"description": "Checks if assets exist by checksums",
|
||||||
"operationId": "checkBulkUpload",
|
"operationId": "checkBulkUpload",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -1894,8 +1894,7 @@
|
|||||||
"summary": "checkBulkUpload",
|
"summary": "checkBulkUpload",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Assets"
|
"Assets"
|
||||||
],
|
]
|
||||||
"x-immich-permission": "asset.upload"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/assets/device/{deviceId}": {
|
"/assets/device/{deviceId}": {
|
||||||
@@ -14571,10 +14570,6 @@
|
|||||||
"query": {
|
"query": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"queryAssetId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"rating": {
|
"rating": {
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
@@ -14642,6 +14637,9 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": [
|
||||||
|
"query"
|
||||||
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SourceType": {
|
"SourceType": {
|
||||||
@@ -15417,10 +15415,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SyncCompleteV1": {
|
|
||||||
"properties": {},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SyncEntityType": {
|
"SyncEntityType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"AuthUserV1",
|
"AuthUserV1",
|
||||||
@@ -15468,8 +15462,7 @@
|
|||||||
"UserMetadataV1",
|
"UserMetadataV1",
|
||||||
"UserMetadataDeleteV1",
|
"UserMetadataDeleteV1",
|
||||||
"SyncAckV1",
|
"SyncAckV1",
|
||||||
"SyncResetV1",
|
"SyncResetV1"
|
||||||
"SyncCompleteV1"
|
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
"default": "./build/index.js"
|
"default": "./build/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
@@ -23,5 +26,8 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/immich-app/immich.git",
|
"url": "git+https://github.com/immich-app/immich.git",
|
||||||
"directory": "open-api/typescript-sdk"
|
"directory": "open-api/typescript-sdk"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1014,8 +1014,7 @@ export type SmartSearchDto = {
|
|||||||
model?: string | null;
|
model?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
query?: string;
|
query: string;
|
||||||
queryAssetId?: string;
|
|
||||||
rating?: number;
|
rating?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
@@ -4922,8 +4921,7 @@ export enum SyncEntityType {
|
|||||||
UserMetadataV1 = "UserMetadataV1",
|
UserMetadataV1 = "UserMetadataV1",
|
||||||
UserMetadataDeleteV1 = "UserMetadataDeleteV1",
|
UserMetadataDeleteV1 = "UserMetadataDeleteV1",
|
||||||
SyncAckV1 = "SyncAckV1",
|
SyncAckV1 = "SyncAckV1",
|
||||||
SyncResetV1 = "SyncResetV1",
|
SyncResetV1 = "SyncResetV1"
|
||||||
SyncCompleteV1 = "SyncCompleteV1"
|
|
||||||
}
|
}
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
AlbumsV1 = "AlbumsV1",
|
AlbumsV1 = "AlbumsV1",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
22.18.0
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202509021104@sha256:47d38c94775332000a93fbbeca1c796687b2d2919e3c75b6e26ab8a65d1864f3 AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202508191104@sha256:0608857ef682099c458f0fb319afdcaf09462bbb5670b6dcd3642029f12eee1c AS dev
|
||||||
|
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
@@ -77,7 +77,7 @@ RUN apt-get update \
|
|||||||
RUN dart --disable-analytics
|
RUN dart --disable-analytics
|
||||||
|
|
||||||
# production-builder-base image
|
# production-builder-base image
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202509021104@sha256:47d38c94775332000a93fbbeca1c796687b2d2919e3c75b6e26ab8a65d1864f3 AS prod-builder-base
|
FROM ghcr.io/immich-app/base-server-dev:202508191104@sha256:0608857ef682099c458f0fb319afdcaf09462bbb5670b6dcd3642029f12eee1c AS prod-builder-base
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
COREPACK_HOME=/tmp
|
COREPACK_HOME=/tmp
|
||||||
@@ -115,7 +115,7 @@ RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install &&
|
|||||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||||
|
|
||||||
# prod base image
|
# prod base image
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202509021104@sha256:84f3727cff75c623f79236cdd9a2b72c84f7665057f474851016f702c67157af
|
FROM ghcr.io/immich-app/base-server-prod:202508191104@sha256:4cce4119f5555fce5e383b681e4feea31956ceadb94cafcbcbbae2c7b94a1b62
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
@@ -5,6 +5,34 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"start": "npm run start:dev",
|
||||||
|
"nest": "nest",
|
||||||
|
"start:dev": "nest start --watch --",
|
||||||
|
"start:debug": "nest start --debug 0.0.0.0:9230 --watch --",
|
||||||
|
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
|
"check": "tsc --noEmit",
|
||||||
|
"check:code": "npm run format && npm run lint && npm run check",
|
||||||
|
"check:all": "npm run check:code && npm run test:cov",
|
||||||
|
"test": "vitest --config test/vitest.config.mjs",
|
||||||
|
"test:cov": "vitest --config test/vitest.config.mjs --coverage",
|
||||||
|
"test:medium": "vitest --config test/vitest.config.medium.mjs",
|
||||||
|
"typeorm": "typeorm",
|
||||||
|
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||||
|
"migrations:debug": "node ./dist/bin/migrations.js debug",
|
||||||
|
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||||
|
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||||
|
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||||
|
"schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
|
||||||
|
"schema:reset": "npm run schema:drop && npm run migrations:run",
|
||||||
|
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||||
|
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||||
|
"email:dev": "email dev -p 3050 --dir src/emails"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^11.0.1",
|
"@nestjs/bullmq": "^11.0.1",
|
||||||
"@nestjs/common": "^11.0.4",
|
"@nestjs/common": "^11.0.4",
|
||||||
@@ -144,6 +172,9 @@
|
|||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.18.0"
|
||||||
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export class AssetMediaController {
|
|||||||
* Checks if assets exist by checksums
|
* Checks if assets exist by checksums
|
||||||
*/
|
*/
|
||||||
@Post('bulk-upload-check')
|
@Post('bulk-upload-check')
|
||||||
@Authenticated({ permission: Permission.AssetUpload })
|
@Authenticated()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'checkBulkUpload',
|
summary: 'checkBulkUpload',
|
||||||
description: 'Checks if assets exist by checksums',
|
description: 'Checks if assets exist by checksums',
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ describe(SearchController.name, () => {
|
|||||||
await request(ctx.getHttpServer()).post('/search/smart');
|
await request(ctx.getHttpServer()).post('/search/smart');
|
||||||
expect(ctx.authenticate).toHaveBeenCalled();
|
expect(ctx.authenticate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a query', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/explore', () => {
|
describe('GET /search/explore', () => {
|
||||||
|
|||||||
@@ -199,12 +199,7 @@ export class StatisticsSearchDto extends BaseSearchDto {
|
|||||||
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Optional()
|
query!: string;
|
||||||
query?: string;
|
|
||||||
|
|
||||||
@ValidateUUID({ optional: true })
|
|
||||||
@Optional()
|
|
||||||
queryAssetId?: string;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -336,9 +336,6 @@ export class SyncAckV1 {}
|
|||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncResetV1 {}
|
export class SyncResetV1 {}
|
||||||
|
|
||||||
@ExtraModel()
|
|
||||||
export class SyncCompleteV1 {}
|
|
||||||
|
|
||||||
export type SyncItem = {
|
export type SyncItem = {
|
||||||
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
||||||
[SyncEntityType.UserV1]: SyncUserV1;
|
[SyncEntityType.UserV1]: SyncUserV1;
|
||||||
@@ -385,7 +382,6 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
||||||
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||||
[SyncEntityType.SyncCompleteV1]: SyncCompleteV1;
|
|
||||||
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -530,7 +530,6 @@ export enum JobName {
|
|||||||
AssetGenerateThumbnails = 'AssetGenerateThumbnails',
|
AssetGenerateThumbnails = 'AssetGenerateThumbnails',
|
||||||
|
|
||||||
AuditLogCleanup = 'AuditLogCleanup',
|
AuditLogCleanup = 'AuditLogCleanup',
|
||||||
AuditTableCleanup = 'AuditTableCleanup',
|
|
||||||
|
|
||||||
DatabaseBackup = 'DatabaseBackup',
|
DatabaseBackup = 'DatabaseBackup',
|
||||||
|
|
||||||
@@ -571,7 +570,8 @@ export enum JobName {
|
|||||||
SendMail = 'SendMail',
|
SendMail = 'SendMail',
|
||||||
|
|
||||||
SidecarQueueAll = 'SidecarQueueAll',
|
SidecarQueueAll = 'SidecarQueueAll',
|
||||||
SidecarCheck = 'SidecarCheck',
|
SidecarDiscovery = 'SidecarDiscovery',
|
||||||
|
SidecarSync = 'SidecarSync',
|
||||||
SidecarWrite = 'SidecarWrite',
|
SidecarWrite = 'SidecarWrite',
|
||||||
|
|
||||||
SmartSearchQueueAll = 'SmartSearchQueueAll',
|
SmartSearchQueueAll = 'SmartSearchQueueAll',
|
||||||
@@ -708,7 +708,6 @@ export enum SyncEntityType {
|
|||||||
|
|
||||||
SyncAckV1 = 'SyncAckV1',
|
SyncAckV1 = 'SyncAckV1',
|
||||||
SyncResetV1 = 'SyncResetV1',
|
SyncResetV1 = 'SyncResetV1',
|
||||||
SyncCompleteV1 = 'SyncCompleteV1',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationLevel {
|
export enum NotificationLevel {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthRequest } from 'src/middleware/auth.guard';
|
|||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { ImmichFile, UploadFile, UploadFiles } from 'src/types';
|
import { ImmichFile, UploadFile, UploadFiles } from 'src/types';
|
||||||
import { asUploadRequest, mapToUploadFile } from 'src/utils/asset.util';
|
import { asRequest, mapToUploadFile } from 'src/utils/asset.util';
|
||||||
|
|
||||||
export function getFile(files: UploadFiles, property: 'assetData' | 'sidecarData') {
|
export function getFile(files: UploadFiles, property: 'assetData' | 'sidecarData') {
|
||||||
const file = files[property]?.[0];
|
const file = files[property]?.[0];
|
||||||
@@ -99,21 +99,18 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
|
private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
|
||||||
return callbackify(() => this.assetService.canUploadFile(asUploadRequest(request, file)), callback);
|
return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||||
return callbackify(
|
return callbackify(
|
||||||
() => this.assetService.getUploadFilename(asUploadRequest(request, file)),
|
() => this.assetService.getUploadFilename(asRequest(request, file)),
|
||||||
callback as Callback<string>,
|
callback as Callback<string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||||
return callbackify(
|
return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback<string>);
|
||||||
() => this.assetService.getUploadFolder(asUploadRequest(request, file)),
|
|
||||||
callback as Callback<string>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
|
private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
|
||||||
|
|||||||
@@ -43,18 +43,6 @@ where
|
|||||||
limit
|
limit
|
||||||
$2
|
$2
|
||||||
|
|
||||||
-- AssetJobRepository.getForSidecarCheckJob
|
|
||||||
select
|
|
||||||
"id",
|
|
||||||
"sidecarPath",
|
|
||||||
"originalPath"
|
|
||||||
from
|
|
||||||
"asset"
|
|
||||||
where
|
|
||||||
"asset"."id" = $1::uuid
|
|
||||||
limit
|
|
||||||
$2
|
|
||||||
|
|
||||||
-- AssetJobRepository.streamForThumbnailJob
|
-- AssetJobRepository.streamForThumbnailJob
|
||||||
select
|
select
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
|
|||||||
@@ -123,14 +123,6 @@ offset
|
|||||||
$8
|
$8
|
||||||
commit
|
commit
|
||||||
|
|
||||||
-- SearchRepository.getEmbedding
|
|
||||||
select
|
|
||||||
*
|
|
||||||
from
|
|
||||||
"smart_search"
|
|
||||||
where
|
|
||||||
"assetId" = $1
|
|
||||||
|
|
||||||
-- SearchRepository.searchFaces
|
-- SearchRepository.searchFaces
|
||||||
begin
|
begin
|
||||||
set
|
set
|
||||||
|
|||||||
@@ -957,7 +957,7 @@ where
|
|||||||
order by
|
order by
|
||||||
"stack"."updateId" asc
|
"stack"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.person.getDeletes
|
-- SyncRepository.people.getDeletes
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
"personId"
|
"personId"
|
||||||
@@ -970,7 +970,7 @@ where
|
|||||||
order by
|
order by
|
||||||
"person_audit"."id" asc
|
"person_audit"."id" asc
|
||||||
|
|
||||||
-- SyncRepository.person.getUpserts
|
-- SyncRepository.people.getUpserts
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ export class AssetJobRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.where('asset.id', '=', asUuid(id))
|
.where('asset.id', '=', asUuid(id))
|
||||||
.select(['id', 'sidecarPath', 'originalPath'])
|
.select((eb) => [
|
||||||
.select((eb) =>
|
'id',
|
||||||
|
'sidecarPath',
|
||||||
|
'originalPath',
|
||||||
jsonArrayFrom(
|
jsonArrayFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('tag')
|
.selectFrom('tag')
|
||||||
@@ -48,17 +50,7 @@ export class AssetJobRepository {
|
|||||||
.innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId')
|
.innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId')
|
||||||
.whereRef('asset.id', '=', 'tag_asset.assetsId'),
|
.whereRef('asset.id', '=', 'tag_asset.assetsId'),
|
||||||
).as('tags'),
|
).as('tags'),
|
||||||
)
|
])
|
||||||
.limit(1)
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
|
||||||
getForSidecarCheckJob(id: string) {
|
|
||||||
return this.db
|
|
||||||
.selectFrom('asset')
|
|
||||||
.where('asset.id', '=', asUuid(id))
|
|
||||||
.select(['id', 'sidecarPath', 'originalPath'])
|
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,13 +293,6 @@ export class SearchRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
|
||||||
params: [DummyValue.UUID],
|
|
||||||
})
|
|
||||||
async getEmbedding(assetId: string) {
|
|
||||||
return this.db.selectFrom('smart_search').selectAll().where('assetId', '=', assetId).executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Kysely, sql } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
@@ -62,7 +62,7 @@ export class SyncRepository {
|
|||||||
partnerAsset: PartnerAssetsSync;
|
partnerAsset: PartnerAssetsSync;
|
||||||
partnerAssetExif: PartnerAssetExifsSync;
|
partnerAssetExif: PartnerAssetExifsSync;
|
||||||
partnerStack: PartnerStackSync;
|
partnerStack: PartnerStackSync;
|
||||||
person: PersonSync;
|
people: PersonSync;
|
||||||
stack: StackSync;
|
stack: StackSync;
|
||||||
user: UserSync;
|
user: UserSync;
|
||||||
userMetadata: UserMetadataSync;
|
userMetadata: UserMetadataSync;
|
||||||
@@ -84,7 +84,7 @@ export class SyncRepository {
|
|||||||
this.partnerAsset = new PartnerAssetsSync(this.db);
|
this.partnerAsset = new PartnerAssetsSync(this.db);
|
||||||
this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
|
this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
|
||||||
this.partnerStack = new PartnerStackSync(this.db);
|
this.partnerStack = new PartnerStackSync(this.db);
|
||||||
this.person = new PersonSync(this.db);
|
this.people = new PersonSync(this.db);
|
||||||
this.stack = new StackSync(this.db);
|
this.stack = new StackSync(this.db);
|
||||||
this.user = new UserSync(this.db);
|
this.user = new UserSync(this.db);
|
||||||
this.userMetadata = new UserMetadataSync(this.db);
|
this.userMetadata = new UserMetadataSync(this.db);
|
||||||
@@ -117,15 +117,6 @@ class BaseSync {
|
|||||||
.orderBy(idRef, 'asc');
|
.orderBy(idRef, 'asc');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected auditCleanup<T extends keyof DB>(t: T, days: number) {
|
|
||||||
const { table, ref } = this.db.dynamic;
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.deleteFrom(table(t).as(t))
|
|
||||||
.where(ref(`${t}.deletedAt`), '<', sql.raw(`now() - interval '${days} days'`))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected upsertQuery<T extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) {
|
protected upsertQuery<T extends keyof DB>(t: T, { nowId, ack }: SyncQueryOptions) {
|
||||||
const { table, ref } = this.db.dynamic;
|
const { table, ref } = this.db.dynamic;
|
||||||
const updateIdRef = ref(`${t}.updateId`);
|
const updateIdRef = ref(`${t}.updateId`);
|
||||||
@@ -159,10 +150,6 @@ class AlbumSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('album_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
@@ -299,10 +286,6 @@ class AlbumToAssetSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('album_asset_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
@@ -351,10 +334,6 @@ class AlbumUserSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('album_user_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
@@ -392,10 +371,6 @@ class AssetSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('asset_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('asset', options)
|
return this.upsertQuery('asset', options)
|
||||||
@@ -425,10 +400,6 @@ class PersonSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('person_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('person', options)
|
return this.upsertQuery('person', options)
|
||||||
@@ -460,10 +431,6 @@ class AssetFaceSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('asset_face_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('asset_face', options)
|
return this.upsertQuery('asset_face', options)
|
||||||
@@ -506,10 +473,6 @@ class MemorySync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('memory_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('memory', options)
|
return this.upsertQuery('memory', options)
|
||||||
@@ -542,10 +505,6 @@ class MemoryToAssetSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('memory_asset_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('memory_asset', options)
|
return this.upsertQuery('memory_asset', options)
|
||||||
@@ -578,10 +537,6 @@ class PartnerSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('partner_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
@@ -661,10 +616,6 @@ class StackSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('stack_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('stack', options)
|
return this.upsertQuery('stack', options)
|
||||||
@@ -713,10 +664,6 @@ class UserSync extends BaseSync {
|
|||||||
return this.auditQuery('user_audit', options).select(['id', 'userId']).stream();
|
return this.auditQuery('user_audit', options).select(['id', 'userId']).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('user_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('user', options).select(columns.syncUser).stream();
|
return this.upsertQuery('user', options).select(columns.syncUser).stream();
|
||||||
@@ -732,10 +679,6 @@ class UserMetadataSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('user_metadata_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
return this.upsertQuery('user_metadata', options)
|
return this.upsertQuery('user_metadata', options)
|
||||||
@@ -755,10 +698,6 @@ class AssetMetadataSync extends BaseSync {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('asset_metadata_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions, userId: string) {
|
getUpserts(options: SyncQueryOptions, userId: string) {
|
||||||
return this.upsertQuery('asset_metadata', options)
|
return this.upsertQuery('asset_metadata', options)
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ export interface DB {
|
|||||||
api_key: ApiKeyTable;
|
api_key: ApiKeyTable;
|
||||||
|
|
||||||
asset: AssetTable;
|
asset: AssetTable;
|
||||||
asset_audit: AssetAuditTable;
|
|
||||||
asset_exif: AssetExifTable;
|
asset_exif: AssetExifTable;
|
||||||
asset_face: AssetFaceTable;
|
asset_face: AssetFaceTable;
|
||||||
asset_face_audit: AssetFaceAuditTable;
|
asset_face_audit: AssetFaceAuditTable;
|
||||||
@@ -174,6 +173,7 @@ export interface DB {
|
|||||||
asset_metadata: AssetMetadataTable;
|
asset_metadata: AssetMetadataTable;
|
||||||
asset_metadata_audit: AssetMetadataAuditTable;
|
asset_metadata_audit: AssetMetadataAuditTable;
|
||||||
asset_job_status: AssetJobStatusTable;
|
asset_job_status: AssetJobStatusTable;
|
||||||
|
asset_audit: AssetAuditTable;
|
||||||
|
|
||||||
audit: AuditTable;
|
audit: AuditTable;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||||
import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools';
|
import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('memory_asset_audit')
|
@Table('memory_asset_audit')
|
||||||
export class MemoryAssetAuditTable {
|
export class MemoryAssetAuditTable {
|
||||||
@PrimaryGeneratedUuidV7Column()
|
@PrimaryGeneratedUuidV7Column()
|
||||||
id!: Generated<string>;
|
id!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
memoryId!: string;
|
memoryId!: string;
|
||||||
@@ -14,5 +14,5 @@ export class MemoryAssetAuditTable {
|
|||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||||
deletedAt!: Generated<Timestamp>;
|
deletedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
|||||||
const uploadFile = {
|
const uploadFile = {
|
||||||
nullAuth: {
|
nullAuth: {
|
||||||
auth: null,
|
auth: null,
|
||||||
body: {},
|
|
||||||
fieldName: UploadFieldName.ASSET_DATA,
|
fieldName: UploadFieldName.ASSET_DATA,
|
||||||
file: {
|
file: {
|
||||||
uuid: 'random-uuid',
|
uuid: 'random-uuid',
|
||||||
@@ -38,7 +37,6 @@ const uploadFile = {
|
|||||||
filename: (fieldName: UploadFieldName, filename: string) => {
|
filename: (fieldName: UploadFieldName, filename: string) => {
|
||||||
return {
|
return {
|
||||||
auth: authStub.admin,
|
auth: authStub.admin,
|
||||||
body: {},
|
|
||||||
fieldName,
|
fieldName,
|
||||||
file: {
|
file: {
|
||||||
uuid: 'random-uuid',
|
uuid: 'random-uuid',
|
||||||
@@ -899,10 +897,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
|
|
||||||
describe('onUploadError', () => {
|
describe('onUploadError', () => {
|
||||||
it('should queue a job to delete the uploaded file', async () => {
|
it('should queue a job to delete the uploaded file', async () => {
|
||||||
const request = {
|
const request = { user: authStub.user1 } as AuthRequest;
|
||||||
body: {},
|
|
||||||
user: authStub.user1,
|
|
||||||
} as AuthRequest;
|
|
||||||
|
|
||||||
const file = {
|
const file = {
|
||||||
fieldname: UploadFieldName.ASSET_DATA,
|
fieldname: UploadFieldName.ASSET_DATA,
|
||||||
|
|||||||
@@ -24,14 +24,20 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { UploadFile, UploadRequest } from 'src/types';
|
import { UploadFile } from 'src/types';
|
||||||
import { requireUploadAccess } from 'src/utils/access';
|
import { requireUploadAccess } from 'src/utils/access';
|
||||||
import { asUploadRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||||
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
import { fromChecksum } from 'src/utils/request';
|
||||||
|
|
||||||
|
interface UploadRequest {
|
||||||
|
auth: AuthDto | null;
|
||||||
|
fieldName: UploadFieldName;
|
||||||
|
file: UploadFile;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssetMediaRedirectResponse {
|
export interface AssetMediaRedirectResponse {
|
||||||
targetSize: AssetMediaSize | 'original';
|
targetSize: AssetMediaSize | 'original';
|
||||||
}
|
}
|
||||||
@@ -83,15 +89,15 @@ export class AssetMediaService extends BaseService {
|
|||||||
throw new BadRequestException(`Unsupported file type ${filename}`);
|
throw new BadRequestException(`Unsupported file type ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUploadFilename({ auth, fieldName, file, body }: UploadRequest): string {
|
getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
|
||||||
requireUploadAccess(auth);
|
requireUploadAccess(auth);
|
||||||
|
|
||||||
const extension = extname(body.filename || file.originalName);
|
const originalExtension = extname(file.originalName);
|
||||||
|
|
||||||
const lookup = {
|
const lookup = {
|
||||||
[UploadFieldName.ASSET_DATA]: extension,
|
[UploadFieldName.ASSET_DATA]: originalExtension,
|
||||||
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
||||||
[UploadFieldName.PROFILE_DATA]: extension,
|
[UploadFieldName.PROFILE_DATA]: originalExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
return sanitize(`${file.uuid}${lookup[fieldName]}`);
|
return sanitize(`${file.uuid}${lookup[fieldName]}`);
|
||||||
@@ -111,8 +117,8 @@ export class AssetMediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onUploadError(request: AuthRequest, file: Express.Multer.File) {
|
async onUploadError(request: AuthRequest, file: Express.Multer.File) {
|
||||||
const uploadFilename = this.getUploadFilename(asUploadRequest(request, file));
|
const uploadFilename = this.getUploadFilename(asRequest(request, file));
|
||||||
const uploadFolder = this.getUploadFolder(asUploadRequest(request, file));
|
const uploadFolder = this.getUploadFolder(asRequest(request, file));
|
||||||
const uploadPath = `${uploadFolder}/${uploadFilename}`;
|
const uploadPath = `${uploadFolder}/${uploadFilename}`;
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [uploadPath] } });
|
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [uploadPath] } });
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ describe(JobService.name, () => {
|
|||||||
{ name: JobName.PersonCleanup },
|
{ name: JobName.PersonCleanup },
|
||||||
{ name: JobName.MemoryCleanup },
|
{ name: JobName.MemoryCleanup },
|
||||||
{ name: JobName.SessionCleanup },
|
{ name: JobName.SessionCleanup },
|
||||||
{ name: JobName.AuditTableCleanup },
|
|
||||||
{ name: JobName.AuditLogCleanup },
|
{ name: JobName.AuditLogCleanup },
|
||||||
{ name: JobName.MemoryGenerate },
|
{ name: JobName.MemoryGenerate },
|
||||||
{ name: JobName.UserSyncUsage },
|
{ name: JobName.UserSyncUsage },
|
||||||
@@ -239,11 +238,11 @@ describe(JobService.name, () => {
|
|||||||
|
|
||||||
const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [
|
const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [
|
||||||
{
|
{
|
||||||
item: { name: JobName.SidecarCheck, data: { id: 'asset-1' } },
|
item: { name: JobName.SidecarSync, data: { id: 'asset-1' } },
|
||||||
jobs: [JobName.AssetExtractMetadata],
|
jobs: [JobName.AssetExtractMetadata],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
item: { name: JobName.SidecarCheck, data: { id: 'asset-1' } },
|
item: { name: JobName.SidecarDiscovery, data: { id: 'asset-1' } },
|
||||||
jobs: [JobName.AssetExtractMetadata],
|
jobs: [JobName.AssetExtractMetadata],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ export class JobService extends BaseService {
|
|||||||
{ name: JobName.PersonCleanup },
|
{ name: JobName.PersonCleanup },
|
||||||
{ name: JobName.MemoryCleanup },
|
{ name: JobName.MemoryCleanup },
|
||||||
{ name: JobName.SessionCleanup },
|
{ name: JobName.SessionCleanup },
|
||||||
{ name: JobName.AuditTableCleanup },
|
|
||||||
{ name: JobName.AuditLogCleanup },
|
{ name: JobName.AuditLogCleanup },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -310,7 +309,8 @@ export class JobService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
private async onDone(item: JobItem) {
|
private async onDone(item: JobItem) {
|
||||||
switch (item.name) {
|
switch (item.name) {
|
||||||
case JobName.SidecarCheck: {
|
case JobName.SidecarSync:
|
||||||
|
case JobName.SidecarDiscovery: {
|
||||||
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: item.data });
|
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: item.data });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user