mirror of
https://github.com/immich-app/immich.git
synced 2026-05-19 22:12:16 -04:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 706be2db5f | |||
| 87175ee56c | |||
| 13587bf13c | |||
| f09769a2f3 | |||
| bfdff12ee0 | |||
| eb6dca6a31 | |||
| c2e3739a58 | |||
| f6bd514cdc | |||
| d93ab7707e | |||
| 6bb47c802f | |||
| 90a69e2ba6 | |||
| 6580394cfe | |||
| 42ff3b705d | |||
| 0f00053bb1 | |||
| c5c59ed040 | |||
| 576b1eb999 | |||
| 24189702da | |||
| ad0d01005e | |||
| 3e6d053f93 | |||
| 1bb3fd985f | |||
| f72aa54a1f | |||
| dafe9d7966 | |||
| 7acda0572d | |||
| 98bc9f6a6e | |||
| 63a3b405c3 | |||
| 0058df798d | |||
| 97100a4362 | |||
| af39384efb | |||
| 01712cf0a7 | |||
| 2015f95ff5 | |||
| d4f29ab6ac | |||
| 10bc17dace | |||
| fa11a9a01b | |||
| 4bd3d6054e | |||
| 48b3b3caba | |||
| 276b42d3ff |
@@ -51,14 +51,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -79,9 +79,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check out code
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@f8cb9308b42121e793f835bd14c0b8090420430c # v0.0.39
|
||||
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
@@ -71,9 +71,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -23,14 +23,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
suffixes: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -189,6 +189,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -21,14 +21,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -54,9 +54,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
|
||||
@@ -119,9 +119,9 @@ jobs:
|
||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -131,7 +131,9 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Load parameters
|
||||
id: parameters
|
||||
|
||||
@@ -17,9 +17,9 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -29,7 +29,9 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Destroy Docs Subdomain
|
||||
env:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: 'Checkout'
|
||||
@@ -29,10 +29,10 @@ jobs:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0
|
||||
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
secrets:
|
||||
PUSH_O_MATIC_APP_ID:
|
||||
PUSH_O_MATIC_APP_CLIENT_ID:
|
||||
required: true
|
||||
PUSH_O_MATIC_APP_KEY:
|
||||
required: true
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Find translation PR
|
||||
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Require PR to have a changelog label
|
||||
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
secrets:
|
||||
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
@@ -63,13 +63,13 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0
|
||||
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
@@ -32,9 +32,9 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -48,14 +48,14 @@ jobs:
|
||||
name: 'preview'
|
||||
})
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
message-id: 'preview-status'
|
||||
message: 'PRs from forks cannot have preview environments.'
|
||||
|
||||
- uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
|
||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
|
||||
+73
-66
@@ -17,14 +17,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -34,13 +34,17 @@ jobs:
|
||||
- 'web/**'
|
||||
- 'i18n/**'
|
||||
- 'open-api/typescript-sdk/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
server:
|
||||
- 'server/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
cli:
|
||||
- 'cli/**'
|
||||
- 'open-api/typescript-sdk/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
e2e:
|
||||
- 'e2e/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
machine-learning:
|
||||
@@ -63,9 +67,9 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -77,7 +81,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -108,9 +112,9 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -121,7 +125,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -155,9 +159,9 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -168,7 +172,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -197,9 +201,9 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -210,7 +214,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -241,9 +245,9 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -254,7 +258,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -279,9 +283,9 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -292,7 +296,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -327,9 +331,9 @@ jobs:
|
||||
working-directory: ./e2e
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -340,7 +344,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -373,9 +377,9 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -387,13 +391,15 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
- name: Run pnpm install
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||
- name: Run medium tests
|
||||
@@ -414,9 +420,9 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -428,7 +434,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -486,9 +492,9 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -500,7 +506,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -568,7 +574,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
mobile-unit-tests:
|
||||
@@ -580,9 +586,9 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -607,39 +613,40 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
MISE_CEILING_PATHS: ${{ github.workspace }}
|
||||
MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }}/machine-learning/mise.toml
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./machine-learning
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||
with:
|
||||
python-version: 3.11
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
working_directory: ./machine-learning
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --extra cpu
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
uv run ruff check --output-format=github immich_ml
|
||||
- name: Format with ruff
|
||||
run: |
|
||||
uv run ruff format --check immich_ml
|
||||
- name: Run mypy type checking
|
||||
run: |
|
||||
uv run mypy --strict immich_ml/
|
||||
run: mise run install --extra cpu
|
||||
- name: Lint code
|
||||
run: mise run lint --output-format=github
|
||||
- name: Format code
|
||||
run: mise run format
|
||||
- name: Run type checking
|
||||
run: mise run check
|
||||
- name: Run tests and coverage
|
||||
run: |
|
||||
uv run pytest --cov=immich_ml --cov-report term-missing
|
||||
run: mise run test
|
||||
github-files-formatting:
|
||||
name: .github Files Formatting
|
||||
needs: pre-job
|
||||
@@ -652,9 +659,9 @@ jobs:
|
||||
working-directory: ./.github
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -665,7 +672,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -682,9 +689,9 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -703,9 +710,9 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -716,7 +723,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -765,9 +772,9 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
@@ -778,7 +785,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -24,14 +24,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -47,9 +47,9 @@ jobs:
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Bot review status
|
||||
@@ -68,6 +68,6 @@ jobs:
|
||||
permissions: {}
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
Vendored
+3
@@ -29,6 +29,9 @@
|
||||
"editor.formatOnSave": true,
|
||||
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||
},
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
"state_referenced_locally": "ignore"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tools]
|
||||
terragrunt = "1.0.2"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.4.2-ubuntu@sha256:78839fe49e1425c02416fa8072591533a72bd9598e563b54a07d78f9e27fb5d3
|
||||
image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ Some basic examples:
|
||||
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
||||
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
|
||||
|
||||
Note that `*` is a wildcard matching zero or more characters (i.e., withinin a filename or single directory name). `**` matches zero or more subdirectories, recursively. It also includes any/all files within a subdirectory, i.e., when used at the end of a pattern. For example, `**/exclude_me/**` will exclude all files in any directory named `exclude_me`, as well as all files in any subdirectories of `exclude_me`, recursively.
|
||||
|
||||
Special characters such as @ should be escaped, for instance:
|
||||
|
||||
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
|
||||
|
||||
@@ -47,6 +47,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
#### ROCm
|
||||
|
||||
- On Linux, The [AMDGPU driver module](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) needs to be installed on the server and, if secure boot is used, the signing key of DKMS [needs to be enrolled in UEFI BIOS](https://wiki.debian.org/SecureBoot)
|
||||
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
|
||||
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
|
||||
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
|
||||
|
||||
@@ -18,6 +18,7 @@ You can search the following types of content:
|
||||
| People | Faces that are recognized in your photos/videos. |
|
||||
| Contextual | Content of the photos and videos. |
|
||||
| File name or extension | Full or partial file's name, or file's extension |
|
||||
| Full path or folder | Full or partial folder names from the original path. |
|
||||
| Description | Description added to assets. |
|
||||
| Optical Character Recognition (OCR) | Text in images |
|
||||
| Locations | Cities, states, and countries from reverse geocoding. |
|
||||
@@ -30,6 +31,12 @@ You can search the following types of content:
|
||||
|
||||
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
|
||||
|
||||
### Full path or folder
|
||||
|
||||
Use this mode when you know a folder name or part of the original asset path.
|
||||
|
||||
Example: for /John/Projects/3D_Printing/2026-07-01/IMG_0001.jpg, searches like Projects, 3D, Printing, or 2026 match the asset.
|
||||
|
||||
## Configuration
|
||||
|
||||
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
|
||||
|
||||
@@ -29,29 +29,31 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
## General
|
||||
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`<sup>\*3</sup>. | `false` | server | api |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||
|
||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||
|
||||
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
|
||||
|
||||
\*3: The [default configuration](https://helmetjs.github.io/#content-security-policy) sets `upgrade-insecure-requests`, which tells the browser to upgrade all requests to HTTPS. This breaks on HTTP-only deployments. If you cannot use HTTPS, you should use a custom helmet config file with `"upgrade-insecure-requests": null`.
|
||||
|
||||
## Workers
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
|
||||
+35
-1
@@ -1240,6 +1240,7 @@
|
||||
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
|
||||
"free_up_space_settings_subtitle": "Free up device storage",
|
||||
"full_path": "Full path: {path}",
|
||||
"full_path_or_folder": "Full path or folder",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||
"general": "General",
|
||||
@@ -1523,6 +1524,38 @@
|
||||
"marked_all_as_read": "Marked all as read",
|
||||
"matches": "Matches",
|
||||
"matching_assets": "Matching Assets",
|
||||
"media_chrome": {
|
||||
"auto": "Auto",
|
||||
"captions": "Captions",
|
||||
"captions_off": "Off",
|
||||
"closed_captions": "closed captions",
|
||||
"decode_error": "Decode error",
|
||||
"disable_captions": "Disable captions",
|
||||
"enable_captions": "Enable captions",
|
||||
"enter_fullscreen_mode": "Enter fullscreen mode",
|
||||
"exit_fullscreen_mode": "Exit fullscreen mode",
|
||||
"loop": "Loop",
|
||||
"media_error_description": "A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.",
|
||||
"media_loading": "media loading",
|
||||
"mute": "Mute",
|
||||
"network_error": "Network error",
|
||||
"network_error_description": "A network error caused the media download to fail.",
|
||||
"not_supported_error": "Source Not Supported",
|
||||
"playback_rate": "Playback rate",
|
||||
"playback_rate_current": "current playback rate",
|
||||
"playback_rate_value": "Playback rate {playbackRate}",
|
||||
"playback_time": "playback time",
|
||||
"quality": "Quality",
|
||||
"second": "second",
|
||||
"seconds": "seconds",
|
||||
"time_value_of_total_time": "{currentTime} of {totalTime}",
|
||||
"time_value_remaining": "{time} remaining",
|
||||
"unmute": "Unmute",
|
||||
"unsupported_error_description": "An unsupported error occurred. The server or network failed, or your browser does not support this format.",
|
||||
"video_not_loaded_unknown_time": "video not loaded, unknown time.",
|
||||
"video_player": "video player",
|
||||
"volume": "volume"
|
||||
},
|
||||
"media_type": "Media type",
|
||||
"memories": "Memories",
|
||||
"memories_all_caught_up": "All caught up",
|
||||
@@ -1761,7 +1794,6 @@
|
||||
"play_original_video": "Play original video",
|
||||
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
||||
"play_transcoded_video": "Play transcoded video",
|
||||
"playback_speed": "Playback speed",
|
||||
"please_auth_to_access": "Please authenticate to access",
|
||||
"port": "Port",
|
||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||
@@ -1943,6 +1975,8 @@
|
||||
"search_by_description_example": "Hiking day in Sapa",
|
||||
"search_by_filename": "Search by file name or extension",
|
||||
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
||||
"search_by_full_path": "Search by full path or folder",
|
||||
"search_by_full_path_example": "/John/Projects/3D_Printing/2026-07-01 - you can search for Projects, 3D, Printing, 2026 etc.",
|
||||
"search_by_ocr": "Search by OCR",
|
||||
"search_by_ocr_example": "Latte",
|
||||
"search_camera_lens_model": "Search lens model...",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[tools]
|
||||
python = "3.11"
|
||||
uv = "0.8.15"
|
||||
|
||||
[tasks.install]
|
||||
run = "uv sync --locked"
|
||||
|
||||
[tasks.lint]
|
||||
run = "uv run ruff check immich_ml"
|
||||
|
||||
[tasks.test]
|
||||
run = "uv run pytest --cov=immich_ml --cov-report term-missing"
|
||||
|
||||
[tasks.format]
|
||||
run = "uv run ruff format immich_ml"
|
||||
|
||||
[tasks.check]
|
||||
run = "uv run mypy --strict immich_ml/"
|
||||
Generated
+61
-19
@@ -243,14 +243,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -785,17 +785,34 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hf-xet"
|
||||
version = "1.1.7"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719, upload-time = "2025-08-06T00:30:55.741Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743, upload-time = "2025-08-06T00:30:50.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331, upload-time = "2025-08-06T00:30:49.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844, upload-time = "2025-08-06T00:30:47.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209, upload-time = "2025-08-06T00:30:45.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602, upload-time = "2025-08-06T00:30:52.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184, upload-time = "2025-08-06T00:30:54.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008, upload-time = "2025-08-06T00:30:57.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -857,21 +874,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.36.2"
|
||||
version = "1.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
||||
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
||||
{ name = "httpx" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -985,7 +1003,7 @@ requires-dist = [
|
||||
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||
{ name = "gunicorn", specifier = ">=21.1.0" },
|
||||
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
||||
{ name = "huggingface-hub", specifier = ">=1.0,<2.0" },
|
||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
||||
{ name = "numpy", specifier = "<2.4.0" },
|
||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
|
||||
@@ -996,7 +1014,7 @@ requires-dist = [
|
||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||
{ name = "orjson", specifier = ">=3.9.5" },
|
||||
{ name = "pillow", specifier = ">=12.2,<12.3" },
|
||||
{ name = "pillow", specifier = ">=12.2,<13" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" },
|
||||
@@ -2769,6 +2787,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-websocket"
|
||||
version = "1.1.0"
|
||||
@@ -2932,6 +2959,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload-time = "2024-05-02T21:44:01.541Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.20260408"
|
||||
|
||||
@@ -11,13 +11,14 @@ config_roots = [
|
||||
"web",
|
||||
"docs",
|
||||
".github",
|
||||
"machine-learning",
|
||||
]
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
flutter = "3.41.7"
|
||||
pnpm = "10.33.1"
|
||||
terragrunt = "1.0.2"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
java = "21.0.2"
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3046,
|
||||
"android.injected.version.name" => "2.7.5",
|
||||
"android.injected.version.code" => 3047,
|
||||
"android.injected.version.name" => "3.0.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
+13
-145
@@ -2779,17 +2779,15 @@
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"references": [
|
||||
1
|
||||
],
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "asset_ocr_entity",
|
||||
"name": "metadata",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"name": "key",
|
||||
"getter_name": "key",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
@@ -2798,134 +2796,8 @@
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "asset_id",
|
||||
"getter_name": "assetId",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
|
||||
"dialectAwareDefaultConstraints": {
|
||||
"sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
|
||||
},
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
{
|
||||
"foreign_key": {
|
||||
"to": {
|
||||
"table": "remote_asset_entity",
|
||||
"column": "id"
|
||||
},
|
||||
"initially_deferred": false,
|
||||
"on_update": null,
|
||||
"on_delete": "cascade"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "x1",
|
||||
"getter_name": "x1",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "y1",
|
||||
"getter_name": "y1",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "x2",
|
||||
"getter_name": "x2",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "y2",
|
||||
"getter_name": "y2",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "x3",
|
||||
"getter_name": "x3",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "y3",
|
||||
"getter_name": "y3",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "x4",
|
||||
"getter_name": "x4",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "y4",
|
||||
"getter_name": "y4",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "box_score",
|
||||
"getter_name": "boxScore",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "text_score",
|
||||
"getter_name": "textScore",
|
||||
"moor_type": "double",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "recognized_text",
|
||||
"getter_name": "recognizedText",
|
||||
"name": "value",
|
||||
"getter_name": "value",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
@@ -2934,16 +2806,12 @@
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "is_visible",
|
||||
"getter_name": "isVisible",
|
||||
"moor_type": "bool",
|
||||
"name": "updated_at",
|
||||
"getter_name": "updatedAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))",
|
||||
"dialectAwareDefaultConstraints": {
|
||||
"sqlite": "CHECK (\"is_visible\" IN (0, 1))"
|
||||
},
|
||||
"default_dart": "const CustomExpression('1')",
|
||||
"default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
@@ -2953,7 +2821,7 @@
|
||||
"constraints": [],
|
||||
"strict": true,
|
||||
"explicit_pk": [
|
||||
"id"
|
||||
"key"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -3388,11 +3256,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "asset_ocr_entity",
|
||||
"name": "metadata",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"asset_ocr_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"x1\" REAL NOT NULL, \"y1\" REAL NOT NULL, \"x2\" REAL NOT NULL, \"y2\" REAL NOT NULL, \"x3\" REAL NOT NULL, \"y3\" REAL NOT NULL, \"x4\" REAL NOT NULL, \"y4\" REAL NOT NULL, \"box_score\" REAL NOT NULL, \"text_score\" REAL NOT NULL, \"recognized_text\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"metadata\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.7.5</string>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||
|
||||
class AppConfig {
|
||||
final ThemeConfig theme;
|
||||
|
||||
const AppConfig({this.theme = const ThemeConfig()});
|
||||
|
||||
AppConfig copyWith({ThemeConfig? theme}) => .new(theme: theme ?? this.theme);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || (other is AppConfig && other.theme == theme);
|
||||
|
||||
@override
|
||||
int get hashCode => theme.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'AppConfig(theme: $theme)';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
|
||||
class SystemConfig {
|
||||
final LogLevel logLevel;
|
||||
|
||||
const SystemConfig({this.logLevel = .info});
|
||||
|
||||
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
|
||||
|
||||
@override
|
||||
int get hashCode => logLevel.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfig(logLevel: $logLevel)';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeConfig {
|
||||
final ThemeMode mode;
|
||||
|
||||
const ThemeConfig({this.mode = .system});
|
||||
|
||||
ThemeConfig copyWith({ThemeMode? mode}) => .new(mode: mode ?? this.mode);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || (other is ThemeConfig && other.mode == mode);
|
||||
|
||||
@override
|
||||
int get hashCode => mode.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'ThemeConfig(mode: $mode)';
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
|
||||
enum MetadataDomain<T extends Object> {
|
||||
appConfig<AppConfig>('config.app'),
|
||||
systemConfig<SystemConfig>('config.system');
|
||||
|
||||
final String prefix;
|
||||
const MetadataDomain(this.prefix);
|
||||
}
|
||||
|
||||
enum MetadataKey<T extends Object> {
|
||||
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
|
||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
|
||||
|
||||
final MetadataDomain domain;
|
||||
final String name;
|
||||
final T defaultValue;
|
||||
final _MetadataCodec<T>? _codecOverride;
|
||||
|
||||
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
|
||||
|
||||
String get key => '${domain.prefix}.$name';
|
||||
|
||||
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
|
||||
|
||||
String encode(T value) => _codec.encode(value);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
|
||||
|
||||
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
|
||||
}
|
||||
|
||||
sealed class _MetadataCodec<T extends Object> {
|
||||
const _MetadataCodec();
|
||||
|
||||
String encode(T value);
|
||||
T? decode(String raw);
|
||||
|
||||
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
||||
int: _PrimitiveCodec.integer,
|
||||
double: _PrimitiveCodec.real,
|
||||
bool: _PrimitiveCodec.boolean,
|
||||
String: _PrimitiveCodec.string,
|
||||
DateTime: _DateTimeCodec(),
|
||||
};
|
||||
|
||||
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
|
||||
final codec = _primitives[sample.runtimeType];
|
||||
if (codec == null) {
|
||||
throw StateError(
|
||||
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
|
||||
);
|
||||
}
|
||||
return codec as _MetadataCodec<T>;
|
||||
}
|
||||
}
|
||||
|
||||
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
|
||||
final List<T> values;
|
||||
|
||||
const _EnumCodec(this.values);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.name;
|
||||
|
||||
@override
|
||||
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
|
||||
}
|
||||
|
||||
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
||||
const _DateTimeCodec();
|
||||
|
||||
@override
|
||||
String encode(DateTime value) => value.toIso8601String();
|
||||
|
||||
@override
|
||||
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
||||
}
|
||||
|
||||
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
||||
final T? Function(String) _parse;
|
||||
|
||||
const _PrimitiveCodec._(this._parse);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.toString();
|
||||
|
||||
@override
|
||||
T? decode(String raw) => _parse(raw);
|
||||
|
||||
static const integer = _PrimitiveCodec<int>._(int.tryParse);
|
||||
static const real = _PrimitiveCodec<double>._(double.tryParse);
|
||||
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
|
||||
static const string = _PrimitiveCodec<String>._(_identity);
|
||||
|
||||
static String? _identity(String s) => s;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
class Ocr {
|
||||
final String id;
|
||||
final String assetId;
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double x3;
|
||||
final double y3;
|
||||
final double x4;
|
||||
final double y4;
|
||||
final double boxScore;
|
||||
final double textScore;
|
||||
final String text;
|
||||
final bool isVisible;
|
||||
|
||||
const Ocr({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
required this.x1,
|
||||
required this.y1,
|
||||
required this.x2,
|
||||
required this.y2,
|
||||
required this.x3,
|
||||
required this.y3,
|
||||
required this.x4,
|
||||
required this.y4,
|
||||
required this.boxScore,
|
||||
required this.textScore,
|
||||
required this.text,
|
||||
required this.isVisible,
|
||||
});
|
||||
|
||||
Ocr copyWith({
|
||||
String? id,
|
||||
String? assetId,
|
||||
double? x1,
|
||||
double? y1,
|
||||
double? x2,
|
||||
double? y2,
|
||||
double? x3,
|
||||
double? y3,
|
||||
double? x4,
|
||||
double? y4,
|
||||
double? boxScore,
|
||||
double? textScore,
|
||||
String? text,
|
||||
bool? isVisible,
|
||||
}) {
|
||||
return Ocr(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
x1: x1 ?? this.x1,
|
||||
y1: y1 ?? this.y1,
|
||||
x2: x2 ?? this.x2,
|
||||
y2: y2 ?? this.y2,
|
||||
x3: x3 ?? this.x3,
|
||||
y3: y3 ?? this.y3,
|
||||
x4: x4 ?? this.x4,
|
||||
y4: y4 ?? this.y4,
|
||||
boxScore: boxScore ?? this.boxScore,
|
||||
textScore: textScore ?? this.textScore,
|
||||
text: text ?? this.text,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Ocr {
|
||||
id: $id,
|
||||
assetId: $assetId,
|
||||
x1: $x1,
|
||||
y1: $y1,
|
||||
x2: $x2,
|
||||
y2: $y2,
|
||||
x3: $x3,
|
||||
y3: $y3,
|
||||
x4: $x4,
|
||||
y4: $y4,
|
||||
boxScore: $boxScore,
|
||||
textScore: $textScore,
|
||||
text: $text,
|
||||
isVisible: $isVisible
|
||||
}''';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Ocr &&
|
||||
other.id == id &&
|
||||
other.assetId == assetId &&
|
||||
other.x1 == x1 &&
|
||||
other.y1 == y1 &&
|
||||
other.x2 == x2 &&
|
||||
other.y2 == y2 &&
|
||||
other.x3 == x3 &&
|
||||
other.y3 == y3 &&
|
||||
other.x4 == x4 &&
|
||||
other.y4 == y4 &&
|
||||
other.boxScore == boxScore &&
|
||||
other.textScore == textScore &&
|
||||
other.text == text &&
|
||||
other.isVisible == isVisible;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
assetId.hashCode ^
|
||||
x1.hashCode ^
|
||||
y1.hashCode ^
|
||||
x2.hashCode ^
|
||||
y2.hashCode ^
|
||||
x3.hashCode ^
|
||||
y3.hashCode ^
|
||||
x4.hashCode ^
|
||||
y4.hashCode ^
|
||||
boxScore.hashCode ^
|
||||
textScore.hashCode ^
|
||||
text.hashCode ^
|
||||
isVisible.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ enum StoreKey<T> {
|
||||
// user settings from [AppSettingsEnum] below:
|
||||
loadPreview<bool>._(100),
|
||||
loadOriginal<bool>._(101),
|
||||
themeMode<String>._(102),
|
||||
tilesPerRow<int>._(103),
|
||||
dynamicLayout<bool>._(104),
|
||||
groupAssetsBy<int>._(105),
|
||||
@@ -35,7 +34,6 @@ enum StoreKey<T> {
|
||||
albumThumbnailCacheSize<int>._(112),
|
||||
selectedAlbumSortOrder<int>._(113),
|
||||
advancedTroubleshooting<bool>._(114),
|
||||
logLevel<int>._(115),
|
||||
preferRemoteImage<bool>._(116),
|
||||
loopVideo<bool>._(117),
|
||||
// map related settings
|
||||
@@ -94,7 +92,11 @@ enum StoreKey<T> {
|
||||
cleanupCutoffDaysAgo<int>._(1011),
|
||||
cleanupDefaultsInitialized<bool>._(1012),
|
||||
|
||||
syncMigrationStatus<String>._(1013);
|
||||
syncMigrationStatus<String>._(1013),
|
||||
|
||||
// Legacy keys that have been migrated to the new metadata store
|
||||
legacyThemeMode<String>._(102),
|
||||
legacyLogLevel<int>._(115);
|
||||
|
||||
const StoreKey._(this.id);
|
||||
final int id;
|
||||
|
||||
@@ -2,20 +2,20 @@ import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Service responsible for handling application logging.
|
||||
///
|
||||
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
|
||||
/// writes them to a persistent [ILogRepository], and manages log levels
|
||||
/// via [IStoreRepository]
|
||||
/// writes them to a persistent [LogRepository], and manages log levels via
|
||||
/// [MetadataRepository].
|
||||
class LogService {
|
||||
final LogRepository _logRepository;
|
||||
final DriftStoreRepository _storeRepository;
|
||||
final MetadataRepository _metadataRepository;
|
||||
|
||||
final List<LogMessage> _msgBuffer = [];
|
||||
|
||||
@@ -38,12 +38,12 @@ class LogService {
|
||||
|
||||
static Future<LogService> init({
|
||||
required LogRepository logRepository,
|
||||
required DriftStoreRepository storeRepository,
|
||||
required MetadataRepository metadataRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
_instance ??= await create(
|
||||
logRepository: logRepository,
|
||||
storeRepository: storeRepository,
|
||||
metadataRepository: metadataRepository,
|
||||
shouldBuffer: shouldBuffer,
|
||||
);
|
||||
return _instance!;
|
||||
@@ -51,17 +51,17 @@ class LogService {
|
||||
|
||||
static Future<LogService> create({
|
||||
required LogRepository logRepository,
|
||||
required DriftStoreRepository storeRepository,
|
||||
required MetadataRepository metadataRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
||||
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||
final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? LogLevel.info.index;
|
||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO;
|
||||
final level = instance._metadataRepository.systemConfig.logLevel;
|
||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||
return instance;
|
||||
}
|
||||
|
||||
LogService._(this._logRepository, this._storeRepository, this._shouldBuffer) {
|
||||
LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) {
|
||||
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class LogService {
|
||||
}
|
||||
|
||||
Future<void> setLogLevel(LogLevel level) async {
|
||||
await _storeRepository.upsert(StoreKey.logLevel, level.index);
|
||||
await _metadataRepository.write(MetadataKey.logLevel, level);
|
||||
Logger.root.level = level.toLevel();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
|
||||
class OcrService {
|
||||
final OcrRepository _repository;
|
||||
|
||||
const OcrService(this._repository);
|
||||
|
||||
Future<List<Ocr>?> get(String assetId) {
|
||||
return _repository.get(assetId);
|
||||
}
|
||||
}
|
||||
@@ -312,10 +312,6 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.updateAssetFacesV2(data.cast());
|
||||
case SyncEntityType.assetFaceDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
||||
case SyncEntityType.assetOcrV1:
|
||||
return _syncStreamRepository.updateAssetOcrV1(data.cast());
|
||||
case SyncEntityType.assetOcrDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetOcrV1(data.cast());
|
||||
default:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)')
|
||||
class AssetOcrEntity extends Table with DriftDefaultsMixin {
|
||||
const AssetOcrEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
RealColumn get x1 => real()();
|
||||
RealColumn get y1 => real()();
|
||||
|
||||
RealColumn get x2 => real()();
|
||||
RealColumn get y2 => real()();
|
||||
|
||||
RealColumn get x3 => real()();
|
||||
RealColumn get y3 => real()();
|
||||
|
||||
RealColumn get x4 => real()();
|
||||
RealColumn get y4 => real()();
|
||||
|
||||
RealColumn get boxScore => real()();
|
||||
RealColumn get textScore => real()();
|
||||
|
||||
TextColumn get recognizedText => text()();
|
||||
|
||||
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class MetadataEntity extends Table with DriftDefaultsMixin {
|
||||
const MetadataEntity();
|
||||
|
||||
TextColumn get key => text()();
|
||||
|
||||
TextColumn get value => text()();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {key};
|
||||
|
||||
@override
|
||||
String get tableName => "metadata";
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'
|
||||
as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
|
||||
typedef $$MetadataEntityTableCreateCompanionBuilder =
|
||||
i1.MetadataEntityCompanion Function({
|
||||
required String key,
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
typedef $$MetadataEntityTableUpdateCompanionBuilder =
|
||||
i1.MetadataEntityCompanion Function({
|
||||
i0.Value<String> key,
|
||||
i0.Value<String> value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
|
||||
class $$MetadataEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get key => $composableBuilder(
|
||||
column: $table.key,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get value => $composableBuilder(
|
||||
column: $table.value,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get key => $composableBuilder(
|
||||
column: $table.key,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get value => $composableBuilder(
|
||||
column: $table.value,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get key =>
|
||||
$composableBuilder(column: $table.key, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get value =>
|
||||
$composableBuilder(column: $table.value, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData,
|
||||
i1.$$MetadataEntityTableFilterComposer,
|
||||
i1.$$MetadataEntityTableOrderingComposer,
|
||||
i1.$$MetadataEntityTableAnnotationComposer,
|
||||
$$MetadataEntityTableCreateCompanionBuilder,
|
||||
$$MetadataEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.MetadataEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData
|
||||
>,
|
||||
),
|
||||
i1.MetadataEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
> {
|
||||
$$MetadataEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$MetadataEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$MetadataEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () => i1
|
||||
.$$MetadataEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> key = const i0.Value.absent(),
|
||||
i0.Value<String> value = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.MetadataEntityCompanion(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String key,
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.MetadataEntityCompanion.insert(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$MetadataEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData,
|
||||
i1.$$MetadataEntityTableFilterComposer,
|
||||
i1.$$MetadataEntityTableOrderingComposer,
|
||||
i1.$$MetadataEntityTableAnnotationComposer,
|
||||
$$MetadataEntityTableCreateCompanionBuilder,
|
||||
$$MetadataEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.MetadataEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData
|
||||
>,
|
||||
),
|
||||
i1.MetadataEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $MetadataEntityTable extends i2.MetadataEntity
|
||||
with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$MetadataEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
|
||||
'value',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i3.currentDateAndTime,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'metadata';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.MetadataEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('key')) {
|
||||
context.handle(
|
||||
_keyMeta,
|
||||
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_keyMeta);
|
||||
}
|
||||
if (data.containsKey('value')) {
|
||||
context.handle(
|
||||
_valueMeta,
|
||||
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_valueMeta);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
i1.MetadataEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.MetadataEntityData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}value'],
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$MetadataEntityTable createAlias(String alias) {
|
||||
return $MetadataEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class MetadataEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.MetadataEntityData> {
|
||||
final String key;
|
||||
final String value;
|
||||
final DateTime updatedAt;
|
||||
const MetadataEntityData({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['key'] = i0.Variable<String>(key);
|
||||
map['value'] = i0.Variable<String>(value);
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory MetadataEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return MetadataEntityData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String>(json['value']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'value': serializer.toJson<String>(value),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
i1.MetadataEntityData copyWith({
|
||||
String? key,
|
||||
String? value,
|
||||
DateTime? updatedAt,
|
||||
}) => i1.MetadataEntityData(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) {
|
||||
return MetadataEntityData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MetadataEntityData(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, value, updatedAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.MetadataEntityData &&
|
||||
other.key == this.key &&
|
||||
other.value == this.value &&
|
||||
other.updatedAt == this.updatedAt);
|
||||
}
|
||||
|
||||
class MetadataEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.MetadataEntityData> {
|
||||
final i0.Value<String> key;
|
||||
final i0.Value<String> value;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
const MetadataEntityCompanion({
|
||||
this.key = const i0.Value.absent(),
|
||||
this.value = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
});
|
||||
MetadataEntityCompanion.insert({
|
||||
required String key,
|
||||
required String value,
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
}) : key = i0.Value(key),
|
||||
value = i0.Value(value);
|
||||
static i0.Insertable<i1.MetadataEntityData> custom({
|
||||
i0.Expression<String>? key,
|
||||
i0.Expression<String>? value,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (key != null) 'key': key,
|
||||
if (value != null) 'value': value,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
i1.MetadataEntityCompanion copyWith({
|
||||
i0.Value<String>? key,
|
||||
i0.Value<String>? value,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i1.MetadataEntityCompanion(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (key.present) {
|
||||
map['key'] = i0.Variable<String>(key.value);
|
||||
}
|
||||
if (value.present) {
|
||||
map['value'] = i0.Variable<String>(value.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MetadataEntityCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
@@ -14,6 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
@@ -54,7 +54,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.da
|
||||
StoreEntity,
|
||||
TrashedLocalAssetEntity,
|
||||
AssetEditEntity,
|
||||
AssetOcrEntity,
|
||||
MetadataEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
@@ -253,7 +253,7 @@ class Drift extends $Drift {
|
||||
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
||||
},
|
||||
from24To25: (m, v25) async {
|
||||
await m.create(v25.assetOcrEntity);
|
||||
await m.createTable(v25.metadata);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+5
-12
@@ -43,7 +43,7 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
|
||||
as i20;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
|
||||
as i21;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
|
||||
as i22;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i23;
|
||||
@@ -91,7 +91,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
late final i21.$AssetEditEntityTable assetEditEntity = i21
|
||||
.$AssetEditEntityTable(this);
|
||||
late final i22.$AssetOcrEntityTable assetOcrEntity = i22.$AssetOcrEntityTable(
|
||||
late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable(
|
||||
this,
|
||||
);
|
||||
i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer(
|
||||
@@ -134,7 +134,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
assetOcrEntity,
|
||||
metadataEntity,
|
||||
i10.idxPartnerSharedWithId,
|
||||
i11.idxLatLng,
|
||||
i12.idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -334,13 +334,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
),
|
||||
result: [i0.TableUpdate('asset_edit_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('asset_ocr_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
]);
|
||||
@override
|
||||
i0.DriftDatabaseOptions get options =>
|
||||
@@ -402,6 +395,6 @@ class $DriftManager {
|
||||
);
|
||||
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
|
||||
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
|
||||
i22.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
|
||||
i22.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
|
||||
i22.$$MetadataEntityTableTableManager get metadataEntity =>
|
||||
i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity);
|
||||
}
|
||||
|
||||
+21
-130
@@ -12411,7 +12411,7 @@ final class Schema25 extends i0.VersionedSchema {
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
assetOcrEntity,
|
||||
metadata,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -12864,28 +12864,13 @@ final class Schema25 extends i0.VersionedSchema {
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape49 assetOcrEntity = Shape49(
|
||||
late final Shape49 metadata = Shape49(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_ocr_entity',
|
||||
entityName: 'metadata',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_210,
|
||||
_column_211,
|
||||
_column_212,
|
||||
_column_213,
|
||||
_column_214,
|
||||
_column_215,
|
||||
_column_216,
|
||||
_column_217,
|
||||
_column_218,
|
||||
_column_219,
|
||||
_column_220,
|
||||
_column_201,
|
||||
],
|
||||
tableConstraints: ['PRIMARY KEY("key")'],
|
||||
columns: [_column_210, _column_211, _column_115],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
@@ -12934,119 +12919,25 @@ final class Schema25 extends i0.VersionedSchema {
|
||||
|
||||
class Shape49 extends i0.VersionedTable {
|
||||
Shape49({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get assetId =>
|
||||
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<double> get x1 =>
|
||||
columnsByName['x1']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y1 =>
|
||||
columnsByName['y1']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x2 =>
|
||||
columnsByName['x2']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y2 =>
|
||||
columnsByName['y2']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x3 =>
|
||||
columnsByName['x3']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y3 =>
|
||||
columnsByName['y3']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get x4 =>
|
||||
columnsByName['x4']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get y4 =>
|
||||
columnsByName['y4']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get boxScore =>
|
||||
columnsByName['box_score']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get textScore =>
|
||||
columnsByName['text_score']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<String> get recognizedText =>
|
||||
columnsByName['recognized_text']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isVisible =>
|
||||
columnsByName['is_visible']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get key =>
|
||||
columnsByName['key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get value =>
|
||||
columnsByName['value']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<double> _column_210(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_211(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_212(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_213(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_214(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_215(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_216(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'x4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_217(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'y4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_218(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'box_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_219(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'text_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_220(String aliasedName) =>
|
||||
i1.GeneratedColumn<String> _column_210(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'recognized_text',
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_211(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class MetadataRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
final Map<MetadataKey, Object> _cache = {};
|
||||
|
||||
MetadataRepository._(this._db) : super(_db);
|
||||
|
||||
static MetadataRepository? _instance;
|
||||
|
||||
static MetadataRepository get instance {
|
||||
final instance = _instance;
|
||||
if (instance == null) {
|
||||
throw StateError('MetadataRepository not initialized. Call ensureInitialized() first');
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
AppConfig _appConfig = const .new();
|
||||
AppConfig get appConfig => _appConfig;
|
||||
|
||||
SystemConfig _systemConfig = const .new();
|
||||
SystemConfig get systemConfig => _systemConfig;
|
||||
|
||||
static Future<MetadataRepository> ensureInitialized(Drift db) async {
|
||||
if (_instance == null) {
|
||||
final instance = MetadataRepository._(db);
|
||||
await instance._hydrate();
|
||||
_instance = instance;
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static Future<void> refresh() async {
|
||||
instance._cache.clear();
|
||||
instance._appConfig = const .new();
|
||||
instance._systemConfig = const .new();
|
||||
await instance._hydrate();
|
||||
}
|
||||
|
||||
Future<void> _hydrate() async => _hydrateCache(await _db.select(_db.metadataEntity).get());
|
||||
|
||||
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
|
||||
|
||||
Future<void> write<T extends Object>(MetadataKey<T> key, T value) async {
|
||||
if (_read(key) == value) return;
|
||||
|
||||
await _db
|
||||
.into(_db.metadataEntity)
|
||||
.insertOnConflictUpdate(
|
||||
MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||
);
|
||||
_updateCache(key, value);
|
||||
}
|
||||
|
||||
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
|
||||
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
|
||||
_updateCache(key, key.defaultValue);
|
||||
}
|
||||
|
||||
Stream<AppConfig> watchAppConfig() => _watchDomain(MetadataDomain.appConfig).distinct();
|
||||
|
||||
Stream<SystemConfig> watchSystemConfig() => _watchDomain(MetadataDomain.systemConfig).distinct();
|
||||
|
||||
Stream<T> _watchDomain<T extends Object>(MetadataDomain<T> domain) {
|
||||
final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%'));
|
||||
return query.watch().map((rows) {
|
||||
_hydrateCache(rows);
|
||||
return domain.config(this);
|
||||
});
|
||||
}
|
||||
|
||||
void _hydrateCache(List<MetadataEntityData> rows) {
|
||||
final keyMap = MetadataKey.asKeyMap();
|
||||
for (final row in rows) {
|
||||
final key = keyMap[row.key];
|
||||
if (key == null) continue;
|
||||
_updateCache(key, key.decode(row.value));
|
||||
}
|
||||
}
|
||||
|
||||
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
|
||||
if (_cache[key] == value) return;
|
||||
_cache[key] = value;
|
||||
key.domain.rebuild(this);
|
||||
}
|
||||
}
|
||||
|
||||
extension<T extends Object> on MetadataDomain<T> {
|
||||
T config(MetadataRepository repo) => switch (this) {
|
||||
.appConfig => repo._appConfig as T,
|
||||
.systemConfig => repo._systemConfig as T,
|
||||
};
|
||||
|
||||
void rebuild(MetadataRepository repo) {
|
||||
switch (this) {
|
||||
case .appConfig:
|
||||
repo._appConfig = .new(theme: .new(mode: repo._read(.themeMode)));
|
||||
case .systemConfig:
|
||||
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class OcrRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const OcrRepository(this._db) : super(_db);
|
||||
|
||||
Future<List<Ocr>> get(String assetId) async {
|
||||
final query = _db.select(_db.assetOcrEntity)
|
||||
..where((row) => row.assetId.equals(assetId) & row.isVisible.equals(true));
|
||||
|
||||
final result = await query.get();
|
||||
return result.map((e) => e.toDto()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetOcrEntityData {
|
||||
Ocr toDto() {
|
||||
return Ocr(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
x1: x1,
|
||||
y1: y1,
|
||||
x2: x2,
|
||||
y2: y2,
|
||||
x3: x3,
|
||||
y3: y3,
|
||||
x4: x4,
|
||||
y4: y4,
|
||||
boxScore: boxScore,
|
||||
textScore: textScore,
|
||||
text: recognizedText,
|
||||
isVisible: isVisible,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,6 @@ class SyncApiRepository {
|
||||
serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)
|
||||
? SyncRequestType.assetFacesV2
|
||||
: SyncRequestType.assetFacesV1,
|
||||
if (serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) SyncRequestType.assetOcrV1,
|
||||
],
|
||||
reset: shouldReset,
|
||||
).toJson(),
|
||||
@@ -211,8 +210,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
|
||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||
SyncEntityType.assetOcrV1: SyncAssetOcrV1.fromJson,
|
||||
SyncEntityType.assetOcrDeleteV1: SyncAssetOcrDeleteV1.fromJson,
|
||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
@@ -63,7 +62,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.userMetadataEntity.deleteAll();
|
||||
await _db.remoteAssetCloudIdEntity.deleteAll();
|
||||
await _db.assetEditEntity.deleteAll();
|
||||
await _db.assetOcrEntity.deleteAll();
|
||||
});
|
||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
@@ -837,52 +835,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetOcrV1(Iterable<SyncAssetOcrV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final assetOcr in data) {
|
||||
final companion = AssetOcrEntityCompanion(
|
||||
assetId: Value(assetOcr.assetId),
|
||||
recognizedText: Value(assetOcr.text),
|
||||
x1: Value(assetOcr.x1),
|
||||
y1: Value(assetOcr.y1),
|
||||
x2: Value(assetOcr.x2),
|
||||
y2: Value(assetOcr.y2),
|
||||
x3: Value(assetOcr.x3),
|
||||
y3: Value(assetOcr.y3),
|
||||
x4: Value(assetOcr.x4),
|
||||
y4: Value(assetOcr.y4),
|
||||
boxScore: Value(assetOcr.boxScore),
|
||||
textScore: Value(assetOcr.textScore),
|
||||
isVisible: Value(assetOcr.isVisible),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.assetOcrEntity,
|
||||
companion.copyWith(id: Value(assetOcr.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetOcrV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAssetOcrV1(Iterable<SyncAssetOcrDeleteV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final assetOcr in data) {
|
||||
batch.deleteWhere(_db.assetOcrEntity, (row) => row.id.equals(assetOcr.id));
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAssetOcrV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pruneAssets() async {
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
|
||||
@@ -53,7 +53,7 @@ void main() async {
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded();
|
||||
await migrateDatabaseIfNeeded(drift);
|
||||
|
||||
runApp(ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const MainWidget()));
|
||||
} catch (error, stack) {
|
||||
|
||||
@@ -26,6 +26,14 @@ class AssetDetails extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
offset: const Offset(0, -3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
|
||||
+10
-1
@@ -22,6 +22,7 @@ class TechnicalDetails extends ConsumerWidget {
|
||||
final exifInfo = this.exifInfo;
|
||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||
final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null;
|
||||
final lensSubtitle = _getLensInfoSubtitle(exifInfo);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -46,9 +47,16 @@ class TechnicalDetails extends ConsumerWidget {
|
||||
title: lensTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||
subtitle: lensSubtitle,
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
] else if (lensSubtitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: lensSubtitle,
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@@ -123,6 +131,7 @@ class TechnicalDetails extends ConsumerWidget {
|
||||
if (exifInfo == null) return null;
|
||||
final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null;
|
||||
final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null;
|
||||
if (fNumber == null && focalLength == null) return null;
|
||||
return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_overlay.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
@@ -358,7 +357,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((s) => s.showingOcr));
|
||||
|
||||
final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index);
|
||||
if (asset == null) {
|
||||
@@ -408,15 +406,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
isPlayingMotionVideo: isPlayingMotionVideo,
|
||||
),
|
||||
),
|
||||
if (showingOcr && displayAsset.width != null && displayAsset.height != null)
|
||||
Positioned.fill(
|
||||
child: OcrOverlay(
|
||||
asset: displayAsset,
|
||||
imageSize: Size(displayAsset.width!.toDouble(), displayAsset.height!.toDouble()),
|
||||
viewportSize: Size(viewportWidth, viewportHeight),
|
||||
controller: _viewController,
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: !_showingDetails,
|
||||
child: Column(
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||
|
||||
class OcrOverlay extends ConsumerStatefulWidget {
|
||||
final BaseAsset asset;
|
||||
final Size imageSize;
|
||||
final Size viewportSize;
|
||||
final PhotoViewControllerBase? controller;
|
||||
|
||||
const OcrOverlay({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.imageSize,
|
||||
required this.viewportSize,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<OcrOverlay> createState() => _OcrOverlayState();
|
||||
}
|
||||
|
||||
class _OcrOverlayState extends ConsumerState<OcrOverlay> {
|
||||
int? _selectedBoxIndex;
|
||||
|
||||
// Current transform read from the PhotoView controller.
|
||||
// Null until the controller has emitted at least one real event or until
|
||||
// we can seed a reliable value from controller.value on init.
|
||||
PhotoViewControllerValue? _controllerValue;
|
||||
StreamSubscription<PhotoViewControllerValue>? _controllerSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_attachController(widget.controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(OcrOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
_detachController();
|
||||
_attachController(widget.controller);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_detachController();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _attachController(PhotoViewControllerBase? controller) {
|
||||
if (controller == null) return;
|
||||
|
||||
// Seed with the current value only when scaleBoundaries is already set.
|
||||
// Before the image finishes loading, PhotoView uses childSize = outerSize
|
||||
// (viewport) as a placeholder, which sets scale = 1.0. That placeholder
|
||||
// is wrong for any image that doesn't exactly fill the viewport.
|
||||
// Once scaleBoundaries is set the value is trustworthy (the image has rendered
|
||||
// at least one frame and setScaleInvisibly has been called with the real
|
||||
// initial/zoomed scale).
|
||||
if (controller.scaleBoundaries != null) {
|
||||
_controllerValue = controller.value;
|
||||
}
|
||||
|
||||
_controllerSub = controller.outputStateStream.listen((value) {
|
||||
if (mounted) setState(() => _controllerValue = value);
|
||||
});
|
||||
}
|
||||
|
||||
void _detachController() {
|
||||
_controllerSub?.cancel();
|
||||
_controllerSub = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.asset is! RemoteAsset) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final ocrData = ref.watch(ocrAssetProvider((widget.asset as RemoteAsset).id));
|
||||
|
||||
return ocrData.when(
|
||||
data: (data) {
|
||||
if (data == null || data.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _OcrBoxes(
|
||||
ocrData: data,
|
||||
controller: widget.controller,
|
||||
imageSize: widget.imageSize,
|
||||
viewportSize: widget.viewportSize,
|
||||
controllerValue: _controllerValue,
|
||||
selectedBoxIndex: _selectedBoxIndex,
|
||||
onSelectionChanged: (index) => setState(() => _selectedBoxIndex = index),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OcrBoxes extends StatelessWidget {
|
||||
final List<Ocr> ocrData;
|
||||
final PhotoViewControllerBase? controller;
|
||||
final Size imageSize;
|
||||
final Size viewportSize;
|
||||
final PhotoViewControllerValue? controllerValue;
|
||||
final int? selectedBoxIndex;
|
||||
final ValueChanged<int?> onSelectionChanged;
|
||||
|
||||
const _OcrBoxes({
|
||||
required this.ocrData,
|
||||
required this.controller,
|
||||
required this.imageSize,
|
||||
required this.viewportSize,
|
||||
required this.controllerValue,
|
||||
required this.selectedBoxIndex,
|
||||
required this.onSelectionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use the actual decoded image size from PhotoView's scaleBoundaries when
|
||||
// available. The image provider may serve a downscaled preview (e.g. Immich
|
||||
// serves a ~1440px preview for large originals), so the decoded dimensions
|
||||
// can differ significantly from the stored asset dimensions. Using the wrong
|
||||
// size would scale every coordinate by the ratio between the two resolutions.
|
||||
final resolvedImageSize = controller?.scaleBoundaries?.childSize ?? imageSize;
|
||||
|
||||
final scale =
|
||||
controllerValue?.scale ??
|
||||
math.min(viewportSize.width / resolvedImageSize.width, viewportSize.height / resolvedImageSize.height);
|
||||
final position = controllerValue?.position ?? Offset.zero;
|
||||
|
||||
final imageWidth = resolvedImageSize.width;
|
||||
final imageHeight = resolvedImageSize.height;
|
||||
final viewportWidth = viewportSize.width;
|
||||
final viewportHeight = viewportSize.height;
|
||||
|
||||
// Image center in viewport space, accounting for pan
|
||||
final cx = viewportWidth / 2 + position.dx;
|
||||
final cy = viewportHeight / 2 + position.dy;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => onSelectionChanged(null),
|
||||
child: ClipRect(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Fills the viewport so taps outside boxes deselect
|
||||
SizedBox(width: viewportWidth, height: viewportHeight),
|
||||
...ocrData.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final ocr = entry.value;
|
||||
|
||||
// Map normalized image coords (0–1) to viewport space
|
||||
final x1 = cx + (ocr.x1 - 0.5) * imageWidth * scale;
|
||||
final y1 = cy + (ocr.y1 - 0.5) * imageHeight * scale;
|
||||
final x2 = cx + (ocr.x2 - 0.5) * imageWidth * scale;
|
||||
final y2 = cy + (ocr.y2 - 0.5) * imageHeight * scale;
|
||||
final x3 = cx + (ocr.x3 - 0.5) * imageWidth * scale;
|
||||
final y3 = cy + (ocr.y3 - 0.5) * imageHeight * scale;
|
||||
final x4 = cx + (ocr.x4 - 0.5) * imageWidth * scale;
|
||||
final y4 = cy + (ocr.y4 - 0.5) * imageHeight * scale;
|
||||
|
||||
// Bounding rectangle for hit testing and Positioned placement
|
||||
final minX = [x1, x2, x3, x4].reduce((a, b) => a < b ? a : b);
|
||||
final maxX = [x1, x2, x3, x4].reduce((a, b) => a > b ? a : b);
|
||||
final minY = [y1, y2, y3, y4].reduce((a, b) => a < b ? a : b);
|
||||
final maxY = [y1, y2, y3, y4].reduce((a, b) => a > b ? a : b);
|
||||
|
||||
return _OcrBoxItem(
|
||||
key: ValueKey(index),
|
||||
ocr: ocr,
|
||||
index: index,
|
||||
isSelected: selectedBoxIndex == index,
|
||||
points: [
|
||||
Offset(x1 - minX, y1 - minY),
|
||||
Offset(x2 - minX, y2 - minY),
|
||||
Offset(x3 - minX, y3 - minY),
|
||||
Offset(x4 - minX, y4 - minY),
|
||||
],
|
||||
left: minX,
|
||||
top: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
angle: math.atan2(y2 - y1, x2 - x1),
|
||||
labelDx: (minX + maxX) / 2 - minX,
|
||||
labelDy: (minY + maxY) / 2 - minY,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OcrBoxItem extends StatelessWidget {
|
||||
final Ocr ocr;
|
||||
final int index;
|
||||
final bool isSelected;
|
||||
final List<Offset> points;
|
||||
final double left;
|
||||
final double top;
|
||||
final double width;
|
||||
final double height;
|
||||
final double angle;
|
||||
final double labelDx;
|
||||
final double labelDy;
|
||||
final ValueChanged<int?> onSelectionChanged;
|
||||
|
||||
const _OcrBoxItem({
|
||||
super.key,
|
||||
required this.ocr,
|
||||
required this.index,
|
||||
required this.isSelected,
|
||||
required this.points,
|
||||
required this.left,
|
||||
required this.top,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.angle,
|
||||
required this.labelDx,
|
||||
required this.labelDy,
|
||||
required this.onSelectionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: GestureDetector(
|
||||
onTap: () => onSelectionChanged(isSelected ? null : index),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: _OcrBoxPainter(
|
||||
points: points,
|
||||
isSelected: isSelected,
|
||||
colorScheme: context.themeData.colorScheme,
|
||||
),
|
||||
size: Size(width, height),
|
||||
),
|
||||
if (isSelected)
|
||||
Positioned(
|
||||
left: labelDx,
|
||||
top: labelDy,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(-0.5, -0.5),
|
||||
child: Transform.rotate(
|
||||
angle: angle,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800]?.withValues(alpha: 0.4),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.max(50, width),
|
||||
maxHeight: math.max(20, height),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: SelectableText(
|
||||
ocr.text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: math.max(12, height * 0.6),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OcrBoxPainter extends CustomPainter {
|
||||
final List<Offset> points;
|
||||
final bool isSelected;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
const _OcrBoxPainter({required this.points, required this.isSelected, required this.colorScheme});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = isSelected ? colorScheme.primary : colorScheme.secondary
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
|
||||
final fillPaint = Paint()
|
||||
..color = (isSelected ? colorScheme.primary : colorScheme.secondary).withValues(alpha: 0.1)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(points[0].dx, points[0].dy)
|
||||
..lineTo(points[1].dx, points[1].dy)
|
||||
..lineTo(points[2].dx, points[2].dy)
|
||||
..lineTo(points[3].dx, points[3].dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_OcrBoxPainter oldDelegate) {
|
||||
return oldDelegate.isSelected != isSelected || !listEquals(oldDelegate.points, points);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
@@ -16,6 +14,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
const ViewerTopAppBar({super.key});
|
||||
@@ -33,7 +32,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
final hasOcr = asset is RemoteAsset && ref.watch(ocrAssetProvider(asset.id)).valueOrNull?.isNotEmpty == true;
|
||||
|
||||
final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
|
||||
|
||||
@@ -45,15 +43,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((state) => state.showingOcr));
|
||||
|
||||
final actions = <Widget>[
|
||||
if (hasOcr)
|
||||
IconButton(
|
||||
icon: Icon(showingOcr ? Icons.text_fields : Icons.text_fields_outlined),
|
||||
onPressed: ref.read(assetViewerProvider.notifier).toggleOcr,
|
||||
color: showingOcr ? context.primaryColor : null,
|
||||
),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
|
||||
@@ -8,7 +8,6 @@ class AssetViewerState {
|
||||
final bool showingDetails;
|
||||
final bool showingControls;
|
||||
final bool isZoomed;
|
||||
final bool showingOcr;
|
||||
final BaseAsset? currentAsset;
|
||||
final int stackIndex;
|
||||
|
||||
@@ -17,7 +16,6 @@ class AssetViewerState {
|
||||
this.showingDetails = false,
|
||||
this.showingControls = true,
|
||||
this.isZoomed = false,
|
||||
this.showingOcr = false,
|
||||
this.currentAsset,
|
||||
this.stackIndex = 0,
|
||||
});
|
||||
@@ -27,7 +25,6 @@ class AssetViewerState {
|
||||
bool? showingDetails,
|
||||
bool? showingControls,
|
||||
bool? isZoomed,
|
||||
bool? showingOcr,
|
||||
BaseAsset? currentAsset,
|
||||
int? stackIndex,
|
||||
}) {
|
||||
@@ -36,7 +33,6 @@ class AssetViewerState {
|
||||
showingDetails: showingDetails ?? this.showingDetails,
|
||||
showingControls: showingControls ?? this.showingControls,
|
||||
isZoomed: isZoomed ?? this.isZoomed,
|
||||
showingOcr: showingOcr ?? this.showingOcr,
|
||||
currentAsset: currentAsset ?? this.currentAsset,
|
||||
stackIndex: stackIndex ?? this.stackIndex,
|
||||
);
|
||||
@@ -44,7 +40,7 @@ class AssetViewerState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed, showingOcr: $showingOcr)';
|
||||
return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -56,7 +52,6 @@ class AssetViewerState {
|
||||
other.showingDetails == showingDetails &&
|
||||
other.showingControls == showingControls &&
|
||||
other.isZoomed == isZoomed &&
|
||||
other.showingOcr == showingOcr &&
|
||||
other.currentAsset == currentAsset &&
|
||||
other.stackIndex == stackIndex;
|
||||
}
|
||||
@@ -67,7 +62,6 @@ class AssetViewerState {
|
||||
showingDetails.hashCode ^
|
||||
showingControls.hashCode ^
|
||||
isZoomed.hashCode ^
|
||||
showingOcr.hashCode ^
|
||||
currentAsset.hashCode ^
|
||||
stackIndex.hashCode;
|
||||
}
|
||||
@@ -90,7 +84,7 @@ class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
|
||||
|
||||
void setAsset(BaseAsset asset) {
|
||||
if (asset == state.currentAsset) return;
|
||||
state = state.copyWith(currentAsset: asset, stackIndex: 0, showingOcr: false);
|
||||
state = state.copyWith(currentAsset: asset, stackIndex: 0);
|
||||
}
|
||||
|
||||
void setOpacity(double opacity) {
|
||||
@@ -137,10 +131,6 @@ class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
|
||||
}
|
||||
state = state.copyWith(stackIndex: index);
|
||||
}
|
||||
|
||||
void toggleOcr() {
|
||||
state = state.copyWith(showingOcr: !state.showingOcr);
|
||||
}
|
||||
}
|
||||
|
||||
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
|
||||
final metadataProvider = Provider<MetadataRepository>((_) => MetadataRepository.instance);
|
||||
|
||||
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
||||
final repo = ref.watch(metadataProvider);
|
||||
final subscription = repo.watchAppConfig().listen((event) => ref.state = event);
|
||||
ref.onDispose(subscription.cancel);
|
||||
return repo.appConfig;
|
||||
});
|
||||
|
||||
final systemConfigProvider = Provider.autoDispose<SystemConfig>((ref) {
|
||||
final repo = ref.watch(metadataProvider);
|
||||
final subscription = repo.watchSystemConfig().listen((event) => ref.state = event);
|
||||
ref.onDispose(subscription.cancel);
|
||||
return repo.systemConfig;
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/domain/services/ocr.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final ocrRepositoryProvider = Provider<OcrRepository>((ref) => OcrRepository(ref.watch(driftProvider)));
|
||||
|
||||
final ocrServiceProvider = Provider<OcrService>((ref) => OcrService(ref.watch(ocrRepositoryProvider)));
|
||||
|
||||
final ocrAssetProvider = FutureProvider.autoDispose.family<List<Ocr>?, String>((ref, assetId) async {
|
||||
final service = ref.watch(ocrServiceProvider);
|
||||
return service.get(assetId);
|
||||
});
|
||||
@@ -1,27 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
|
||||
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
|
||||
|
||||
dPrint(() => "Current themeMode $themeMode");
|
||||
|
||||
if (themeMode == ThemeMode.light.name) {
|
||||
return ThemeMode.light;
|
||||
} else if (themeMode == ThemeMode.dark.name) {
|
||||
return ThemeMode.dark;
|
||||
} else {
|
||||
return ThemeMode.system;
|
||||
}
|
||||
});
|
||||
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) => ref.watch(appConfigProvider).theme.mode);
|
||||
|
||||
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
|
||||
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
enum AppSettingsEnum<T> {
|
||||
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
|
||||
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
|
||||
themeMode<String>(StoreKey.themeMode, "themeMode", "system"), // "light","dark","system"
|
||||
primaryColor<String>(StoreKey.primaryColor, "primaryColor", defaultColorPresetName),
|
||||
dynamicTheme<bool>(StoreKey.dynamicTheme, "dynamicTheme", false),
|
||||
colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true),
|
||||
@@ -30,7 +29,6 @@ enum AppSettingsEnum<T> {
|
||||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
@@ -48,9 +49,11 @@ abstract final class Bootstrap {
|
||||
|
||||
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
|
||||
|
||||
final metadataRepo = await MetadataRepository.ensureInitialized(drift);
|
||||
|
||||
await LogService.init(
|
||||
logRepository: LogRepository(logDb),
|
||||
storeRepository: storeRepo,
|
||||
metadataRepository: metadataRepo,
|
||||
shouldBuffer: shouldBufferLogs,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
|
||||
ValueNotifier<T> useAppSettingsState<T>(AppSettingsEnum<T> key) {
|
||||
final notifier = useState<T>(Store.get(key.storeKey, key.defaultValue));
|
||||
|
||||
@@ -1,25 +1,76 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
|
||||
const int targetVersion = 25;
|
||||
const int targetVersion = 26;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded() async {
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
|
||||
if (version < 25) {
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
if (accessToken != null && accessToken.isNotEmpty) {
|
||||
final serverUrls = ApiService.getServerUrls();
|
||||
if (serverUrls.isNotEmpty) {
|
||||
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
|
||||
}
|
||||
}
|
||||
await _migrateTo25();
|
||||
}
|
||||
|
||||
if (version < 26) {
|
||||
await _migrateTo26(drift);
|
||||
}
|
||||
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> _migrateTo25() async {
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
if (accessToken == null || accessToken.isEmpty) return;
|
||||
|
||||
final serverUrls = ApiService.getServerUrls();
|
||||
if (serverUrls.isEmpty) return;
|
||||
|
||||
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
|
||||
}
|
||||
|
||||
Future<void> _migrateTo26(Drift drift) async {
|
||||
final repo = MetadataRepository.instance;
|
||||
final migrated = <int>[];
|
||||
|
||||
final themeMode = await _readLegacyStoreString(drift, StoreKey.legacyThemeMode.id);
|
||||
if (themeMode != null) {
|
||||
final mode = ThemeMode.values.firstWhere((m) => m.name == themeMode, orElse: () => ThemeMode.system);
|
||||
await repo.write(MetadataKey.themeMode, mode);
|
||||
migrated.add(StoreKey.legacyThemeMode.id);
|
||||
}
|
||||
|
||||
final logLevelIndex = await _readLegacyStoreInt(drift, StoreKey.legacyLogLevel.id);
|
||||
if (logLevelIndex != null) {
|
||||
final logLevel = LogLevel.values.elementAtOrNull(logLevelIndex) ?? LogLevel.info;
|
||||
await LogService.I.setLogLevel(logLevel);
|
||||
migrated.add(StoreKey.legacyLogLevel.id);
|
||||
}
|
||||
|
||||
await _deleteLegacyStoreRows(drift, migrated);
|
||||
}
|
||||
|
||||
Future<String?> _readLegacyStoreString(Drift drift, int id) async {
|
||||
final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
return row?.stringValue;
|
||||
}
|
||||
|
||||
Future<int?> _readLegacyStoreInt(Drift drift, int id) async {
|
||||
final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
return row?.intValue;
|
||||
}
|
||||
|
||||
Future<void> _deleteLegacyStoreRows(Drift drift, List<int> ids) async {
|
||||
if (ids.isEmpty) return;
|
||||
await (drift.storeEntity.delete()..where((t) => t.id.isIn(ids))).go();
|
||||
}
|
||||
|
||||
@@ -139,8 +139,6 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||
|
||||
PhotoViewHeroAttributes? get heroAttributes => widget.heroAttributes;
|
||||
|
||||
late ScaleBoundaries cachedScaleBoundaries = widget.scaleBoundaries;
|
||||
|
||||
void handleScaleAnimation() {
|
||||
scale = _scaleAnimation!.value;
|
||||
}
|
||||
@@ -303,7 +301,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||
controller.scaleAnimationBuilder(_animateControllerScale);
|
||||
controller.rotationAnimationBuilder(_animateControllerRotation);
|
||||
|
||||
cachedScaleBoundaries = widget.scaleBoundaries;
|
||||
_updateScaleBoundaries();
|
||||
|
||||
_scaleAnimationController = AnimationController(vsync: this)
|
||||
..addListener(handleScaleAnimation)
|
||||
@@ -334,14 +332,27 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||
widget.onTapDown?.call(context, details, controller.value);
|
||||
}
|
||||
|
||||
void _updateScaleBoundaries() {
|
||||
final prev = controller.scaleBoundaries;
|
||||
if (prev == widget.scaleBoundaries) return;
|
||||
|
||||
if (prev != null && controller.scale != null && prev.initialScale > 0) {
|
||||
final ratio = widget.scaleBoundaries.initialScale / prev.initialScale;
|
||||
controller.setScaleInvisibly(controller.scale! * ratio);
|
||||
} else {
|
||||
markNeedsScaleRecalc = true;
|
||||
}
|
||||
controller.scaleBoundaries = widget.scaleBoundaries;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PhotoViewCore oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateScaleBoundaries();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if we need a recalc on the scale
|
||||
if (widget.scaleBoundaries != cachedScaleBoundaries) {
|
||||
markNeedsScaleRecalc = true;
|
||||
cachedScaleBoundaries = widget.scaleBoundaries;
|
||||
}
|
||||
|
||||
return StreamBuilder(
|
||||
stream: controller.outputStateStream,
|
||||
initialData: controller.prevValue,
|
||||
|
||||
@@ -145,7 +145,6 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
||||
_lastStack = null;
|
||||
|
||||
_didLoadSynchronously = synchronousCall;
|
||||
widget.controller.scaleBoundaries = scaleBoundaries;
|
||||
}
|
||||
|
||||
synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB);
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
@@ -30,7 +31,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
final isManageMediaSupported = useState(false);
|
||||
final manageMediaAndroidPermission = useState(false);
|
||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||
final levelId = useState<int>(ref.read(systemConfigProvider).logLevel.index);
|
||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||
final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled);
|
||||
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class ThemeSetting extends HookConsumerWidget {
|
||||
const ThemeSetting({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode);
|
||||
final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider));
|
||||
final currentTheme = useState(ref.read(immichThemeModeProvider));
|
||||
final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark);
|
||||
final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system);
|
||||
|
||||
final applyThemeToBackgroundSetting = useAppSettingsState(AppSettingsEnum.colorfulInterface);
|
||||
final applyThemeToBackgroundProvider = useValueNotifier(ref.read(colorfulInterfaceSettingProvider));
|
||||
|
||||
useValueChanged(
|
||||
currentThemeString.value,
|
||||
(_, __) => currentTheme.value = switch (currentThemeString.value) {
|
||||
"light" => ThemeMode.light,
|
||||
"dark" => ThemeMode.dark,
|
||||
_ => ThemeMode.system,
|
||||
},
|
||||
);
|
||||
|
||||
useValueChanged(
|
||||
applyThemeToBackgroundSetting.value,
|
||||
(_, __) => applyThemeToBackgroundProvider.value = applyThemeToBackgroundSetting.value,
|
||||
@@ -40,16 +32,17 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
void onThemeChange(bool isDark) {
|
||||
if (isDark) {
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
||||
currentThemeString.value = "dark";
|
||||
currentTheme.value = ThemeMode.dark;
|
||||
} else {
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
||||
currentThemeString.value = "light";
|
||||
currentTheme.value = ThemeMode.light;
|
||||
}
|
||||
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
|
||||
}
|
||||
|
||||
void onSystemThemeChange(bool isSystem) {
|
||||
if (isSystem) {
|
||||
currentThemeString.value = "system";
|
||||
currentTheme.value = ThemeMode.system;
|
||||
isSystemTheme.value = true;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system;
|
||||
} else {
|
||||
@@ -57,13 +50,14 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
isSystemTheme.value = false;
|
||||
isDarkTheme.value = currentSystemBrightness == Brightness.dark;
|
||||
if (currentSystemBrightness == Brightness.light) {
|
||||
currentThemeString.value = "light";
|
||||
currentTheme.value = ThemeMode.light;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
||||
} else if (currentSystemBrightness == Brightness.dark) {
|
||||
currentThemeString.value = "dark";
|
||||
currentTheme.value = ThemeMode.dark;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
||||
}
|
||||
}
|
||||
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
|
||||
}
|
||||
|
||||
void onSurfaceColorSettingChange(bool useColorfulInterface) {
|
||||
|
||||
Generated
+3
-4
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 2.7.5
|
||||
- API version: 3.0.0
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
@@ -146,7 +146,7 @@ Class | Method | HTTP request | Description
|
||||
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
|
||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
|
||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
|
||||
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate
|
||||
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Dismiss a duplicate group
|
||||
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
|
||||
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
|
||||
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups
|
||||
@@ -578,9 +578,8 @@ Class | Method | HTTP request | Description
|
||||
- [SyncAssetFaceV2](doc//SyncAssetFaceV2.md)
|
||||
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
|
||||
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
|
||||
- [SyncAssetOcrDeleteV1](doc//SyncAssetOcrDeleteV1.md)
|
||||
- [SyncAssetOcrV1](doc//SyncAssetOcrV1.md)
|
||||
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||
- [SyncAssetV2](doc//SyncAssetV2.md)
|
||||
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
|
||||
- [SyncEntityType](doc//SyncEntityType.md)
|
||||
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
|
||||
|
||||
Generated
+1
-2
@@ -326,9 +326,8 @@ part 'model/sync_asset_face_v1.dart';
|
||||
part 'model/sync_asset_face_v2.dart';
|
||||
part 'model/sync_asset_metadata_delete_v1.dart';
|
||||
part 'model/sync_asset_metadata_v1.dart';
|
||||
part 'model/sync_asset_ocr_delete_v1.dart';
|
||||
part 'model/sync_asset_ocr_v1.dart';
|
||||
part 'model/sync_asset_v1.dart';
|
||||
part 'model/sync_asset_v2.dart';
|
||||
part 'model/sync_auth_user_v1.dart';
|
||||
part 'model/sync_entity_type.dart';
|
||||
part 'model/sync_memory_asset_delete_v1.dart';
|
||||
|
||||
+4
-4
@@ -16,9 +16,9 @@ class DuplicatesApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Delete a duplicate
|
||||
/// Dismiss a duplicate group
|
||||
///
|
||||
/// Delete a single duplicate asset specified by its ID.
|
||||
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
@@ -51,9 +51,9 @@ class DuplicatesApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete a duplicate
|
||||
/// Dismiss a duplicate group
|
||||
///
|
||||
/// Delete a single duplicate asset specified by its ID.
|
||||
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
Generated
+2
-4
@@ -698,12 +698,10 @@ class ApiClient {
|
||||
return SyncAssetMetadataDeleteV1.fromJson(value);
|
||||
case 'SyncAssetMetadataV1':
|
||||
return SyncAssetMetadataV1.fromJson(value);
|
||||
case 'SyncAssetOcrDeleteV1':
|
||||
return SyncAssetOcrDeleteV1.fromJson(value);
|
||||
case 'SyncAssetOcrV1':
|
||||
return SyncAssetOcrV1.fromJson(value);
|
||||
case 'SyncAssetV1':
|
||||
return SyncAssetV1.fromJson(value);
|
||||
case 'SyncAssetV2':
|
||||
return SyncAssetV2.fromJson(value);
|
||||
case 'SyncAuthUserV1':
|
||||
return SyncAuthUserV1.fromJson(value);
|
||||
case 'SyncEntityType':
|
||||
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAssetOcrDeleteV1 {
|
||||
/// Returns a new [SyncAssetOcrDeleteV1] instance.
|
||||
SyncAssetOcrDeleteV1({
|
||||
required this.assetId,
|
||||
required this.deletedAt,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
/// Original asset ID of the deleted OCR entry
|
||||
String assetId;
|
||||
|
||||
/// Timestamp when the OCR entry was deleted
|
||||
DateTime deletedAt;
|
||||
|
||||
/// Audit row ID of the deleted OCR entry
|
||||
String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrDeleteV1 &&
|
||||
other.assetId == assetId &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.id == id;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(deletedAt.hashCode) +
|
||||
(id.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetOcrDeleteV1[assetId=$assetId, deletedAt=$deletedAt, id=$id]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.deletedAt.millisecondsSinceEpoch
|
||||
: this.deletedAt.toUtc().toIso8601String();
|
||||
json[r'id'] = this.id;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAssetOcrDeleteV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAssetOcrDeleteV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAssetOcrDeleteV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAssetOcrDeleteV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAssetOcrDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAssetOcrDeleteV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAssetOcrDeleteV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAssetOcrDeleteV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAssetOcrDeleteV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAssetOcrDeleteV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAssetOcrDeleteV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAssetOcrDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAssetOcrDeleteV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAssetOcrDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'deletedAt',
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
||||
-217
@@ -1,217 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAssetOcrV1 {
|
||||
/// Returns a new [SyncAssetOcrV1] instance.
|
||||
SyncAssetOcrV1({
|
||||
required this.assetId,
|
||||
required this.boxScore,
|
||||
required this.id,
|
||||
required this.isVisible,
|
||||
required this.text,
|
||||
required this.textScore,
|
||||
required this.x1,
|
||||
required this.x2,
|
||||
required this.x3,
|
||||
required this.x4,
|
||||
required this.y1,
|
||||
required this.y2,
|
||||
required this.y3,
|
||||
required this.y4,
|
||||
});
|
||||
|
||||
/// Asset ID
|
||||
String assetId;
|
||||
|
||||
/// Confidence score of the bounding box
|
||||
double boxScore;
|
||||
|
||||
/// OCR entry ID
|
||||
String id;
|
||||
|
||||
/// Whether the OCR entry is visible
|
||||
bool isVisible;
|
||||
|
||||
/// Recognized text content
|
||||
String text;
|
||||
|
||||
/// Confidence score of the recognized text
|
||||
double textScore;
|
||||
|
||||
/// Top-left X coordinate (normalized 0–1)
|
||||
double x1;
|
||||
|
||||
/// Top-right X coordinate (normalized 0–1)
|
||||
double x2;
|
||||
|
||||
/// Bottom-right X coordinate (normalized 0–1)
|
||||
double x3;
|
||||
|
||||
/// Bottom-left X coordinate (normalized 0–1)
|
||||
double x4;
|
||||
|
||||
/// Top-left Y coordinate (normalized 0–1)
|
||||
double y1;
|
||||
|
||||
/// Top-right Y coordinate (normalized 0–1)
|
||||
double y2;
|
||||
|
||||
/// Bottom-right Y coordinate (normalized 0–1)
|
||||
double y3;
|
||||
|
||||
/// Bottom-left Y coordinate (normalized 0–1)
|
||||
double y4;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrV1 &&
|
||||
other.assetId == assetId &&
|
||||
other.boxScore == boxScore &&
|
||||
other.id == id &&
|
||||
other.isVisible == isVisible &&
|
||||
other.text == text &&
|
||||
other.textScore == textScore &&
|
||||
other.x1 == x1 &&
|
||||
other.x2 == x2 &&
|
||||
other.x3 == x3 &&
|
||||
other.x4 == x4 &&
|
||||
other.y1 == y1 &&
|
||||
other.y2 == y2 &&
|
||||
other.y3 == y3 &&
|
||||
other.y4 == y4;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(boxScore.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isVisible.hashCode) +
|
||||
(text.hashCode) +
|
||||
(textScore.hashCode) +
|
||||
(x1.hashCode) +
|
||||
(x2.hashCode) +
|
||||
(x3.hashCode) +
|
||||
(x4.hashCode) +
|
||||
(y1.hashCode) +
|
||||
(y2.hashCode) +
|
||||
(y3.hashCode) +
|
||||
(y4.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetOcrV1[assetId=$assetId, boxScore=$boxScore, id=$id, isVisible=$isVisible, text=$text, textScore=$textScore, x1=$x1, x2=$x2, x3=$x3, x4=$x4, y1=$y1, y2=$y2, y3=$y3, y4=$y4]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'boxScore'] = this.boxScore;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isVisible'] = this.isVisible;
|
||||
json[r'text'] = this.text;
|
||||
json[r'textScore'] = this.textScore;
|
||||
json[r'x1'] = this.x1;
|
||||
json[r'x2'] = this.x2;
|
||||
json[r'x3'] = this.x3;
|
||||
json[r'x4'] = this.x4;
|
||||
json[r'y1'] = this.y1;
|
||||
json[r'y2'] = this.y2;
|
||||
json[r'y3'] = this.y3;
|
||||
json[r'y4'] = this.y4;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAssetOcrV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAssetOcrV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAssetOcrV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAssetOcrV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
boxScore: (mapValueOfType<num>(json, r'boxScore')!).toDouble(),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
||||
text: mapValueOfType<String>(json, r'text')!,
|
||||
textScore: (mapValueOfType<num>(json, r'textScore')!).toDouble(),
|
||||
x1: (mapValueOfType<num>(json, r'x1')!).toDouble(),
|
||||
x2: (mapValueOfType<num>(json, r'x2')!).toDouble(),
|
||||
x3: (mapValueOfType<num>(json, r'x3')!).toDouble(),
|
||||
x4: (mapValueOfType<num>(json, r'x4')!).toDouble(),
|
||||
y1: (mapValueOfType<num>(json, r'y1')!).toDouble(),
|
||||
y2: (mapValueOfType<num>(json, r'y2')!).toDouble(),
|
||||
y3: (mapValueOfType<num>(json, r'y3')!).toDouble(),
|
||||
y4: (mapValueOfType<num>(json, r'y4')!).toDouble(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAssetOcrV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAssetOcrV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAssetOcrV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAssetOcrV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAssetOcrV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAssetOcrV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAssetOcrV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAssetOcrV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAssetOcrV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAssetOcrV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'boxScore',
|
||||
'id',
|
||||
'isVisible',
|
||||
'text',
|
||||
'textScore',
|
||||
'x1',
|
||||
'x2',
|
||||
'x3',
|
||||
'x4',
|
||||
'y1',
|
||||
'y2',
|
||||
'y3',
|
||||
'y4',
|
||||
};
|
||||
}
|
||||
|
||||
+18
-6
@@ -27,18 +27,19 @@ class SyncEntityType {
|
||||
static const userV1 = SyncEntityType._(r'UserV1');
|
||||
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
||||
static const assetV1 = SyncEntityType._(r'AssetV1');
|
||||
static const assetV2 = SyncEntityType._(r'AssetV2');
|
||||
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
|
||||
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
|
||||
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
|
||||
static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1');
|
||||
static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1');
|
||||
static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1');
|
||||
static const assetOcrV1 = SyncEntityType._(r'AssetOcrV1');
|
||||
static const assetOcrDeleteV1 = SyncEntityType._(r'AssetOcrDeleteV1');
|
||||
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
||||
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
||||
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||
static const partnerAssetV2 = SyncEntityType._(r'PartnerAssetV2');
|
||||
static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1');
|
||||
static const partnerAssetBackfillV2 = SyncEntityType._(r'PartnerAssetBackfillV2');
|
||||
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
||||
@@ -52,8 +53,11 @@ class SyncEntityType {
|
||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
|
||||
static const albumAssetCreateV2 = SyncEntityType._(r'AlbumAssetCreateV2');
|
||||
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
|
||||
static const albumAssetUpdateV2 = SyncEntityType._(r'AlbumAssetUpdateV2');
|
||||
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
||||
static const albumAssetBackfillV2 = SyncEntityType._(r'AlbumAssetBackfillV2');
|
||||
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
|
||||
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
|
||||
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
||||
@@ -83,18 +87,19 @@ class SyncEntityType {
|
||||
userV1,
|
||||
userDeleteV1,
|
||||
assetV1,
|
||||
assetV2,
|
||||
assetDeleteV1,
|
||||
assetExifV1,
|
||||
assetEditV1,
|
||||
assetEditDeleteV1,
|
||||
assetMetadataV1,
|
||||
assetMetadataDeleteV1,
|
||||
assetOcrV1,
|
||||
assetOcrDeleteV1,
|
||||
partnerV1,
|
||||
partnerDeleteV1,
|
||||
partnerAssetV1,
|
||||
partnerAssetV2,
|
||||
partnerAssetBackfillV1,
|
||||
partnerAssetBackfillV2,
|
||||
partnerAssetDeleteV1,
|
||||
partnerAssetExifV1,
|
||||
partnerAssetExifBackfillV1,
|
||||
@@ -108,8 +113,11 @@ class SyncEntityType {
|
||||
albumUserBackfillV1,
|
||||
albumUserDeleteV1,
|
||||
albumAssetCreateV1,
|
||||
albumAssetCreateV2,
|
||||
albumAssetUpdateV1,
|
||||
albumAssetUpdateV2,
|
||||
albumAssetBackfillV1,
|
||||
albumAssetBackfillV2,
|
||||
albumAssetExifCreateV1,
|
||||
albumAssetExifUpdateV1,
|
||||
albumAssetExifBackfillV1,
|
||||
@@ -174,18 +182,19 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'UserV1': return SyncEntityType.userV1;
|
||||
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
||||
case r'AssetV1': return SyncEntityType.assetV1;
|
||||
case r'AssetV2': return SyncEntityType.assetV2;
|
||||
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
|
||||
case r'AssetExifV1': return SyncEntityType.assetExifV1;
|
||||
case r'AssetEditV1': return SyncEntityType.assetEditV1;
|
||||
case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1;
|
||||
case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1;
|
||||
case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1;
|
||||
case r'AssetOcrV1': return SyncEntityType.assetOcrV1;
|
||||
case r'AssetOcrDeleteV1': return SyncEntityType.assetOcrDeleteV1;
|
||||
case r'PartnerV1': return SyncEntityType.partnerV1;
|
||||
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
||||
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||
case r'PartnerAssetV2': return SyncEntityType.partnerAssetV2;
|
||||
case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1;
|
||||
case r'PartnerAssetBackfillV2': return SyncEntityType.partnerAssetBackfillV2;
|
||||
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
||||
@@ -199,8 +208,11 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
|
||||
case r'AlbumAssetCreateV2': return SyncEntityType.albumAssetCreateV2;
|
||||
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
|
||||
case r'AlbumAssetUpdateV2': return SyncEntityType.albumAssetUpdateV2;
|
||||
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
||||
case r'AlbumAssetBackfillV2': return SyncEntityType.albumAssetBackfillV2;
|
||||
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
|
||||
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
|
||||
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
||||
|
||||
+9
-3
@@ -28,17 +28,19 @@ class SyncRequestType {
|
||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||
static const albumAssetsV2 = SyncRequestType._(r'AlbumAssetsV2');
|
||||
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
||||
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
||||
static const assetsV2 = SyncRequestType._(r'AssetsV2');
|
||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
|
||||
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
|
||||
static const assetOcrV1 = SyncRequestType._(r'AssetOcrV1');
|
||||
static const authUsersV1 = SyncRequestType._(r'AuthUsersV1');
|
||||
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
|
||||
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
||||
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
||||
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||
static const partnerAssetsV2 = SyncRequestType._(r'PartnerAssetsV2');
|
||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
||||
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
||||
@@ -55,17 +57,19 @@ class SyncRequestType {
|
||||
albumUsersV1,
|
||||
albumToAssetsV1,
|
||||
albumAssetsV1,
|
||||
albumAssetsV2,
|
||||
albumAssetExifsV1,
|
||||
assetsV1,
|
||||
assetsV2,
|
||||
assetExifsV1,
|
||||
assetEditsV1,
|
||||
assetMetadataV1,
|
||||
assetOcrV1,
|
||||
authUsersV1,
|
||||
memoriesV1,
|
||||
memoryToAssetsV1,
|
||||
partnersV1,
|
||||
partnerAssetsV1,
|
||||
partnerAssetsV2,
|
||||
partnerAssetExifsV1,
|
||||
partnerStacksV1,
|
||||
stacksV1,
|
||||
@@ -117,17 +121,19 @@ class SyncRequestTypeTypeTransformer {
|
||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||
case r'AlbumAssetsV2': return SyncRequestType.albumAssetsV2;
|
||||
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
||||
case r'AssetsV1': return SyncRequestType.assetsV1;
|
||||
case r'AssetsV2': return SyncRequestType.assetsV2;
|
||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
|
||||
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
|
||||
case r'AssetOcrV1': return SyncRequestType.assetOcrV1;
|
||||
case r'AuthUsersV1': return SyncRequestType.authUsersV1;
|
||||
case r'MemoriesV1': return SyncRequestType.memoriesV1;
|
||||
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
||||
case r'PartnersV1': return SyncRequestType.partnersV1;
|
||||
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||
case r'PartnerAssetsV2': return SyncRequestType.partnerAssetsV2;
|
||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
||||
case r'StacksV1': return SyncRequestType.stacksV1;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.7.5+3046
|
||||
version: 3.0.0+3047
|
||||
|
||||
environment:
|
||||
sdk: '>=3.11.0 <4.0.0'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -29,21 +29,23 @@ final _kWarnLog = LogMessage(
|
||||
void main() {
|
||||
late LogService sut;
|
||||
late LogRepository mockLogRepo;
|
||||
late DriftStoreRepository mockStoreRepo;
|
||||
late MockMetadataRepository mockMetadataRepository;
|
||||
|
||||
setUp(() async {
|
||||
mockLogRepo = MockLogRepository();
|
||||
mockStoreRepo = MockDriftStoreRepository();
|
||||
mockMetadataRepository = MockMetadataRepository();
|
||||
|
||||
registerFallbackValue(_kInfoLog);
|
||||
registerFallbackValue(LogLevel.info);
|
||||
|
||||
when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {});
|
||||
when(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel)).thenAnswer((_) async => LogLevel.fine.index);
|
||||
when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine));
|
||||
when(() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, any())).thenAnswer((_) async {});
|
||||
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
|
||||
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||
|
||||
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo);
|
||||
sut = await LogService.create(logRepository: mockLogRepo, metadataRepository: mockMetadataRepository);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
@@ -56,21 +58,22 @@ void main() {
|
||||
expect(limit, kLogTruncateLimit);
|
||||
});
|
||||
|
||||
test('Sets log level based on the store setting', () {
|
||||
verify(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel)).called(1);
|
||||
test('Sets log level based on the metadata repository', () {
|
||||
verify(() => mockMetadataRepository.systemConfig).called(1);
|
||||
expect(Logger.root.level, Level.FINE);
|
||||
});
|
||||
});
|
||||
|
||||
group("Log Service Set Level:", () {
|
||||
setUp(() async {
|
||||
when(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
|
||||
await sut.setLogLevel(LogLevel.shout);
|
||||
});
|
||||
|
||||
test('Updates the log level in store', () {
|
||||
final index = verify(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
|
||||
expect(index, LogLevel.shout.index);
|
||||
test('Updates the log level via metadata repository', () {
|
||||
final captured = verify(
|
||||
() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, captureAny()),
|
||||
).captured.firstOrNull;
|
||||
expect(captured, LogLevel.shout);
|
||||
});
|
||||
|
||||
test('Sets log level on logger', () {
|
||||
@@ -81,7 +84,11 @@ void main() {
|
||||
group("Log Service Buffer:", () {
|
||||
test('Buffers logs until timer elapses', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
metadataRepository: mockMetadataRepository,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
@@ -95,7 +102,11 @@ void main() {
|
||||
|
||||
test('Batch inserts all logs on timer', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
metadataRepository: mockMetadataRepository,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
@@ -112,7 +123,11 @@ void main() {
|
||||
|
||||
test('Does not buffer when off', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: false);
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
metadataRepository: mockMetadataRepository,
|
||||
shouldBuffer: false,
|
||||
);
|
||||
|
||||
final logger = Logger(_kInfoLog.logger!);
|
||||
logger.info(_kInfoLog.message);
|
||||
@@ -142,7 +157,11 @@ void main() {
|
||||
|
||||
test('Combines result from both DB + Buffer', () {
|
||||
TestUtils.fakeAsync((time) async {
|
||||
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
metadataRepository: mockMetadataRepository,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
final logger = Logger(_kWarnLog.logger!);
|
||||
logger.warning(_kWarnLog.message);
|
||||
|
||||
+105
-524
@@ -8792,216 +8792,67 @@ class AssetEditEntityCompanion extends UpdateCompanion<AssetEditEntityData> {
|
||||
}
|
||||
}
|
||||
|
||||
class AssetOcrEntity extends Table
|
||||
with TableInfo<AssetOcrEntity, AssetOcrEntityData> {
|
||||
class Metadata extends Table with TableInfo<Metadata, MetadataData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
AssetOcrEntity(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id',
|
||||
Metadata(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> key = GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> assetId = GeneratedColumn<String>(
|
||||
'asset_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints:
|
||||
'NOT NULL REFERENCES remote_asset_entity(id)ON DELETE CASCADE',
|
||||
);
|
||||
late final GeneratedColumn<double> x1 = GeneratedColumn<double>(
|
||||
'x1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> y1 = GeneratedColumn<double>(
|
||||
'y1',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> x2 = GeneratedColumn<double>(
|
||||
'x2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> y2 = GeneratedColumn<double>(
|
||||
'y2',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> x3 = GeneratedColumn<double>(
|
||||
'x3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> y3 = GeneratedColumn<double>(
|
||||
'y3',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> x4 = GeneratedColumn<double>(
|
||||
'x4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> y4 = GeneratedColumn<double>(
|
||||
'y4',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> boxScore = GeneratedColumn<double>(
|
||||
'box_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<double> textScore = GeneratedColumn<double>(
|
||||
'text_score',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.double,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<String> recognizedText = GeneratedColumn<String>(
|
||||
'recognized_text',
|
||||
late final GeneratedColumn<String> value = GeneratedColumn<String>(
|
||||
'value',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
late final GeneratedColumn<int> isVisible = GeneratedColumn<int>(
|
||||
'is_visible',
|
||||
late final GeneratedColumn<String> updatedAt = GeneratedColumn<String>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: 'NOT NULL DEFAULT 1 CHECK (is_visible IN (0, 1))',
|
||||
defaultValue: const CustomExpression('1'),
|
||||
$customConstraints: 'NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
||||
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'),
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
assetId,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
x3,
|
||||
y3,
|
||||
x4,
|
||||
y4,
|
||||
boxScore,
|
||||
textScore,
|
||||
recognizedText,
|
||||
isVisible,
|
||||
];
|
||||
List<GeneratedColumn> get $columns => [key, value, updatedAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'asset_ocr_entity';
|
||||
static const String $name = 'metadata';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
AssetOcrEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
MetadataData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return AssetOcrEntityData(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
return MetadataData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
data['${effectivePrefix}key'],
|
||||
)!,
|
||||
assetId: attachedDatabase.typeMapping.read(
|
||||
value: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}asset_id'],
|
||||
data['${effectivePrefix}value'],
|
||||
)!,
|
||||
x1: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}x1'],
|
||||
)!,
|
||||
y1: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}y1'],
|
||||
)!,
|
||||
x2: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}x2'],
|
||||
)!,
|
||||
y2: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}y2'],
|
||||
)!,
|
||||
x3: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}x3'],
|
||||
)!,
|
||||
y3: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}y3'],
|
||||
)!,
|
||||
x4: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}x4'],
|
||||
)!,
|
||||
y4: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}y4'],
|
||||
)!,
|
||||
boxScore: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}box_score'],
|
||||
)!,
|
||||
textScore: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.double,
|
||||
data['${effectivePrefix}text_score'],
|
||||
)!,
|
||||
recognizedText: attachedDatabase.typeMapping.read(
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}recognized_text'],
|
||||
)!,
|
||||
isVisible: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}is_visible'],
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetOcrEntity createAlias(String alias) {
|
||||
return AssetOcrEntity(attachedDatabase, alias);
|
||||
Metadata createAlias(String alias) {
|
||||
return Metadata(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -9009,408 +8860,145 @@ class AssetOcrEntity extends Table
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
@override
|
||||
List<String> get customConstraints => const ['PRIMARY KEY(id)'];
|
||||
List<String> get customConstraints => const ['PRIMARY KEY("key")'];
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class AssetOcrEntityData extends DataClass
|
||||
implements Insertable<AssetOcrEntityData> {
|
||||
final String id;
|
||||
final String assetId;
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double x3;
|
||||
final double y3;
|
||||
final double x4;
|
||||
final double y4;
|
||||
final double boxScore;
|
||||
final double textScore;
|
||||
final String recognizedText;
|
||||
final int isVisible;
|
||||
const AssetOcrEntityData({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
required this.x1,
|
||||
required this.y1,
|
||||
required this.x2,
|
||||
required this.y2,
|
||||
required this.x3,
|
||||
required this.y3,
|
||||
required this.x4,
|
||||
required this.y4,
|
||||
required this.boxScore,
|
||||
required this.textScore,
|
||||
required this.recognizedText,
|
||||
required this.isVisible,
|
||||
class MetadataData extends DataClass implements Insertable<MetadataData> {
|
||||
final String key;
|
||||
final String value;
|
||||
final String updatedAt;
|
||||
const MetadataData({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<String>(id);
|
||||
map['asset_id'] = Variable<String>(assetId);
|
||||
map['x1'] = Variable<double>(x1);
|
||||
map['y1'] = Variable<double>(y1);
|
||||
map['x2'] = Variable<double>(x2);
|
||||
map['y2'] = Variable<double>(y2);
|
||||
map['x3'] = Variable<double>(x3);
|
||||
map['y3'] = Variable<double>(y3);
|
||||
map['x4'] = Variable<double>(x4);
|
||||
map['y4'] = Variable<double>(y4);
|
||||
map['box_score'] = Variable<double>(boxScore);
|
||||
map['text_score'] = Variable<double>(textScore);
|
||||
map['recognized_text'] = Variable<String>(recognizedText);
|
||||
map['is_visible'] = Variable<int>(isVisible);
|
||||
map['key'] = Variable<String>(key);
|
||||
map['value'] = Variable<String>(value);
|
||||
map['updated_at'] = Variable<String>(updatedAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory AssetOcrEntityData.fromJson(
|
||||
factory MetadataData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return AssetOcrEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
assetId: serializer.fromJson<String>(json['assetId']),
|
||||
x1: serializer.fromJson<double>(json['x1']),
|
||||
y1: serializer.fromJson<double>(json['y1']),
|
||||
x2: serializer.fromJson<double>(json['x2']),
|
||||
y2: serializer.fromJson<double>(json['y2']),
|
||||
x3: serializer.fromJson<double>(json['x3']),
|
||||
y3: serializer.fromJson<double>(json['y3']),
|
||||
x4: serializer.fromJson<double>(json['x4']),
|
||||
y4: serializer.fromJson<double>(json['y4']),
|
||||
boxScore: serializer.fromJson<double>(json['boxScore']),
|
||||
textScore: serializer.fromJson<double>(json['textScore']),
|
||||
recognizedText: serializer.fromJson<String>(json['recognizedText']),
|
||||
isVisible: serializer.fromJson<int>(json['isVisible']),
|
||||
return MetadataData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String>(json['value']),
|
||||
updatedAt: serializer.fromJson<String>(json['updatedAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'assetId': serializer.toJson<String>(assetId),
|
||||
'x1': serializer.toJson<double>(x1),
|
||||
'y1': serializer.toJson<double>(y1),
|
||||
'x2': serializer.toJson<double>(x2),
|
||||
'y2': serializer.toJson<double>(y2),
|
||||
'x3': serializer.toJson<double>(x3),
|
||||
'y3': serializer.toJson<double>(y3),
|
||||
'x4': serializer.toJson<double>(x4),
|
||||
'y4': serializer.toJson<double>(y4),
|
||||
'boxScore': serializer.toJson<double>(boxScore),
|
||||
'textScore': serializer.toJson<double>(textScore),
|
||||
'recognizedText': serializer.toJson<String>(recognizedText),
|
||||
'isVisible': serializer.toJson<int>(isVisible),
|
||||
'key': serializer.toJson<String>(key),
|
||||
'value': serializer.toJson<String>(value),
|
||||
'updatedAt': serializer.toJson<String>(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
AssetOcrEntityData copyWith({
|
||||
String? id,
|
||||
String? assetId,
|
||||
double? x1,
|
||||
double? y1,
|
||||
double? x2,
|
||||
double? y2,
|
||||
double? x3,
|
||||
double? y3,
|
||||
double? x4,
|
||||
double? y4,
|
||||
double? boxScore,
|
||||
double? textScore,
|
||||
String? recognizedText,
|
||||
int? isVisible,
|
||||
}) => AssetOcrEntityData(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
x1: x1 ?? this.x1,
|
||||
y1: y1 ?? this.y1,
|
||||
x2: x2 ?? this.x2,
|
||||
y2: y2 ?? this.y2,
|
||||
x3: x3 ?? this.x3,
|
||||
y3: y3 ?? this.y3,
|
||||
x4: x4 ?? this.x4,
|
||||
y4: y4 ?? this.y4,
|
||||
boxScore: boxScore ?? this.boxScore,
|
||||
textScore: textScore ?? this.textScore,
|
||||
recognizedText: recognizedText ?? this.recognizedText,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
);
|
||||
AssetOcrEntityData copyWithCompanion(AssetOcrEntityCompanion data) {
|
||||
return AssetOcrEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||
x1: data.x1.present ? data.x1.value : this.x1,
|
||||
y1: data.y1.present ? data.y1.value : this.y1,
|
||||
x2: data.x2.present ? data.x2.value : this.x2,
|
||||
y2: data.y2.present ? data.y2.value : this.y2,
|
||||
x3: data.x3.present ? data.x3.value : this.x3,
|
||||
y3: data.y3.present ? data.y3.value : this.y3,
|
||||
x4: data.x4.present ? data.x4.value : this.x4,
|
||||
y4: data.y4.present ? data.y4.value : this.y4,
|
||||
boxScore: data.boxScore.present ? data.boxScore.value : this.boxScore,
|
||||
textScore: data.textScore.present ? data.textScore.value : this.textScore,
|
||||
recognizedText: data.recognizedText.present
|
||||
? data.recognizedText.value
|
||||
: this.recognizedText,
|
||||
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
|
||||
MetadataData copyWith({String? key, String? value, String? updatedAt}) =>
|
||||
MetadataData(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
MetadataData copyWithCompanion(MetadataCompanion data) {
|
||||
return MetadataData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetOcrEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('x1: $x1, ')
|
||||
..write('y1: $y1, ')
|
||||
..write('x2: $x2, ')
|
||||
..write('y2: $y2, ')
|
||||
..write('x3: $x3, ')
|
||||
..write('y3: $y3, ')
|
||||
..write('x4: $x4, ')
|
||||
..write('y4: $y4, ')
|
||||
..write('boxScore: $boxScore, ')
|
||||
..write('textScore: $textScore, ')
|
||||
..write('recognizedText: $recognizedText, ')
|
||||
..write('isVisible: $isVisible')
|
||||
return (StringBuffer('MetadataData(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
assetId,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
x3,
|
||||
y3,
|
||||
x4,
|
||||
y4,
|
||||
boxScore,
|
||||
textScore,
|
||||
recognizedText,
|
||||
isVisible,
|
||||
);
|
||||
int get hashCode => Object.hash(key, value, updatedAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is AssetOcrEntityData &&
|
||||
other.id == this.id &&
|
||||
other.assetId == this.assetId &&
|
||||
other.x1 == this.x1 &&
|
||||
other.y1 == this.y1 &&
|
||||
other.x2 == this.x2 &&
|
||||
other.y2 == this.y2 &&
|
||||
other.x3 == this.x3 &&
|
||||
other.y3 == this.y3 &&
|
||||
other.x4 == this.x4 &&
|
||||
other.y4 == this.y4 &&
|
||||
other.boxScore == this.boxScore &&
|
||||
other.textScore == this.textScore &&
|
||||
other.recognizedText == this.recognizedText &&
|
||||
other.isVisible == this.isVisible);
|
||||
(other is MetadataData &&
|
||||
other.key == this.key &&
|
||||
other.value == this.value &&
|
||||
other.updatedAt == this.updatedAt);
|
||||
}
|
||||
|
||||
class AssetOcrEntityCompanion extends UpdateCompanion<AssetOcrEntityData> {
|
||||
final Value<String> id;
|
||||
final Value<String> assetId;
|
||||
final Value<double> x1;
|
||||
final Value<double> y1;
|
||||
final Value<double> x2;
|
||||
final Value<double> y2;
|
||||
final Value<double> x3;
|
||||
final Value<double> y3;
|
||||
final Value<double> x4;
|
||||
final Value<double> y4;
|
||||
final Value<double> boxScore;
|
||||
final Value<double> textScore;
|
||||
final Value<String> recognizedText;
|
||||
final Value<int> isVisible;
|
||||
const AssetOcrEntityCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.assetId = const Value.absent(),
|
||||
this.x1 = const Value.absent(),
|
||||
this.y1 = const Value.absent(),
|
||||
this.x2 = const Value.absent(),
|
||||
this.y2 = const Value.absent(),
|
||||
this.x3 = const Value.absent(),
|
||||
this.y3 = const Value.absent(),
|
||||
this.x4 = const Value.absent(),
|
||||
this.y4 = const Value.absent(),
|
||||
this.boxScore = const Value.absent(),
|
||||
this.textScore = const Value.absent(),
|
||||
this.recognizedText = const Value.absent(),
|
||||
this.isVisible = const Value.absent(),
|
||||
class MetadataCompanion extends UpdateCompanion<MetadataData> {
|
||||
final Value<String> key;
|
||||
final Value<String> value;
|
||||
final Value<String> updatedAt;
|
||||
const MetadataCompanion({
|
||||
this.key = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
this.updatedAt = const Value.absent(),
|
||||
});
|
||||
AssetOcrEntityCompanion.insert({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required double x1,
|
||||
required double y1,
|
||||
required double x2,
|
||||
required double y2,
|
||||
required double x3,
|
||||
required double y3,
|
||||
required double x4,
|
||||
required double y4,
|
||||
required double boxScore,
|
||||
required double textScore,
|
||||
required String recognizedText,
|
||||
this.isVisible = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
assetId = Value(assetId),
|
||||
x1 = Value(x1),
|
||||
y1 = Value(y1),
|
||||
x2 = Value(x2),
|
||||
y2 = Value(y2),
|
||||
x3 = Value(x3),
|
||||
y3 = Value(y3),
|
||||
x4 = Value(x4),
|
||||
y4 = Value(y4),
|
||||
boxScore = Value(boxScore),
|
||||
textScore = Value(textScore),
|
||||
recognizedText = Value(recognizedText);
|
||||
static Insertable<AssetOcrEntityData> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? assetId,
|
||||
Expression<double>? x1,
|
||||
Expression<double>? y1,
|
||||
Expression<double>? x2,
|
||||
Expression<double>? y2,
|
||||
Expression<double>? x3,
|
||||
Expression<double>? y3,
|
||||
Expression<double>? x4,
|
||||
Expression<double>? y4,
|
||||
Expression<double>? boxScore,
|
||||
Expression<double>? textScore,
|
||||
Expression<String>? recognizedText,
|
||||
Expression<int>? isVisible,
|
||||
MetadataCompanion.insert({
|
||||
required String key,
|
||||
required String value,
|
||||
this.updatedAt = const Value.absent(),
|
||||
}) : key = Value(key),
|
||||
value = Value(value);
|
||||
static Insertable<MetadataData> custom({
|
||||
Expression<String>? key,
|
||||
Expression<String>? value,
|
||||
Expression<String>? updatedAt,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (assetId != null) 'asset_id': assetId,
|
||||
if (x1 != null) 'x1': x1,
|
||||
if (y1 != null) 'y1': y1,
|
||||
if (x2 != null) 'x2': x2,
|
||||
if (y2 != null) 'y2': y2,
|
||||
if (x3 != null) 'x3': x3,
|
||||
if (y3 != null) 'y3': y3,
|
||||
if (x4 != null) 'x4': x4,
|
||||
if (y4 != null) 'y4': y4,
|
||||
if (boxScore != null) 'box_score': boxScore,
|
||||
if (textScore != null) 'text_score': textScore,
|
||||
if (recognizedText != null) 'recognized_text': recognizedText,
|
||||
if (isVisible != null) 'is_visible': isVisible,
|
||||
if (key != null) 'key': key,
|
||||
if (value != null) 'value': value,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
AssetOcrEntityCompanion copyWith({
|
||||
Value<String>? id,
|
||||
Value<String>? assetId,
|
||||
Value<double>? x1,
|
||||
Value<double>? y1,
|
||||
Value<double>? x2,
|
||||
Value<double>? y2,
|
||||
Value<double>? x3,
|
||||
Value<double>? y3,
|
||||
Value<double>? x4,
|
||||
Value<double>? y4,
|
||||
Value<double>? boxScore,
|
||||
Value<double>? textScore,
|
||||
Value<String>? recognizedText,
|
||||
Value<int>? isVisible,
|
||||
MetadataCompanion copyWith({
|
||||
Value<String>? key,
|
||||
Value<String>? value,
|
||||
Value<String>? updatedAt,
|
||||
}) {
|
||||
return AssetOcrEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
x1: x1 ?? this.x1,
|
||||
y1: y1 ?? this.y1,
|
||||
x2: x2 ?? this.x2,
|
||||
y2: y2 ?? this.y2,
|
||||
x3: x3 ?? this.x3,
|
||||
y3: y3 ?? this.y3,
|
||||
x4: x4 ?? this.x4,
|
||||
y4: y4 ?? this.y4,
|
||||
boxScore: boxScore ?? this.boxScore,
|
||||
textScore: textScore ?? this.textScore,
|
||||
recognizedText: recognizedText ?? this.recognizedText,
|
||||
isVisible: isVisible ?? this.isVisible,
|
||||
return MetadataCompanion(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
if (key.present) {
|
||||
map['key'] = Variable<String>(key.value);
|
||||
}
|
||||
if (assetId.present) {
|
||||
map['asset_id'] = Variable<String>(assetId.value);
|
||||
if (value.present) {
|
||||
map['value'] = Variable<String>(value.value);
|
||||
}
|
||||
if (x1.present) {
|
||||
map['x1'] = Variable<double>(x1.value);
|
||||
}
|
||||
if (y1.present) {
|
||||
map['y1'] = Variable<double>(y1.value);
|
||||
}
|
||||
if (x2.present) {
|
||||
map['x2'] = Variable<double>(x2.value);
|
||||
}
|
||||
if (y2.present) {
|
||||
map['y2'] = Variable<double>(y2.value);
|
||||
}
|
||||
if (x3.present) {
|
||||
map['x3'] = Variable<double>(x3.value);
|
||||
}
|
||||
if (y3.present) {
|
||||
map['y3'] = Variable<double>(y3.value);
|
||||
}
|
||||
if (x4.present) {
|
||||
map['x4'] = Variable<double>(x4.value);
|
||||
}
|
||||
if (y4.present) {
|
||||
map['y4'] = Variable<double>(y4.value);
|
||||
}
|
||||
if (boxScore.present) {
|
||||
map['box_score'] = Variable<double>(boxScore.value);
|
||||
}
|
||||
if (textScore.present) {
|
||||
map['text_score'] = Variable<double>(textScore.value);
|
||||
}
|
||||
if (recognizedText.present) {
|
||||
map['recognized_text'] = Variable<String>(recognizedText.value);
|
||||
}
|
||||
if (isVisible.present) {
|
||||
map['is_visible'] = Variable<int>(isVisible.value);
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = Variable<String>(updatedAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetOcrEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('x1: $x1, ')
|
||||
..write('y1: $y1, ')
|
||||
..write('x2: $x2, ')
|
||||
..write('y2: $y2, ')
|
||||
..write('x3: $x3, ')
|
||||
..write('y3: $y3, ')
|
||||
..write('x4: $x4, ')
|
||||
..write('y4: $y4, ')
|
||||
..write('boxScore: $boxScore, ')
|
||||
..write('textScore: $textScore, ')
|
||||
..write('recognizedText: $recognizedText, ')
|
||||
..write('isVisible: $isVisible')
|
||||
return (StringBuffer('MetadataCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -9488,7 +9076,7 @@ class DatabaseAtV25 extends GeneratedDatabase {
|
||||
late final TrashedLocalAssetEntity trashedLocalAssetEntity =
|
||||
TrashedLocalAssetEntity(this);
|
||||
late final AssetEditEntity assetEditEntity = AssetEditEntity(this);
|
||||
late final AssetOcrEntity assetOcrEntity = AssetOcrEntity(this);
|
||||
late final Metadata metadata = Metadata(this);
|
||||
late final Index idxPartnerSharedWithId = Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
@@ -9566,7 +9154,7 @@ class DatabaseAtV25 extends GeneratedDatabase {
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
assetOcrEntity,
|
||||
metadata,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
@@ -9748,13 +9336,6 @@ class DatabaseAtV25 extends GeneratedDatabase {
|
||||
),
|
||||
result: [TableUpdate('asset_edit_entity', kind: UpdateKind.delete)],
|
||||
),
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: UpdateKind.delete,
|
||||
),
|
||||
result: [TableUpdate('asset_ocr_entity', kind: UpdateKind.delete)],
|
||||
),
|
||||
]);
|
||||
@override
|
||||
int get schemaVersion => 25;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:immich_mobile/infrastructure/repositories/backup.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/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
@@ -17,6 +18,8 @@ import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
|
||||
|
||||
class MockMetadataRepository extends Mock implements MetadataRepository {}
|
||||
|
||||
class MockLogRepository extends Mock implements LogRepository {}
|
||||
|
||||
class MockSyncStreamRepository extends Mock implements SyncStreamRepository {}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late MetadataRepository sut;
|
||||
|
||||
setUpAll(() async {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = await MetadataRepository.ensureInitialized(ctx.db);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await ctx.db.delete(ctx.db.metadataEntity).go();
|
||||
await MetadataRepository.refresh();
|
||||
});
|
||||
|
||||
group('defaults', () {
|
||||
test('appConfig returns key defaults when DB is empty', () {
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||
});
|
||||
|
||||
test('systemConfig returns key defaults when DB is empty', () {
|
||||
expect(sut.systemConfig.logLevel, LogLevel.info);
|
||||
});
|
||||
});
|
||||
|
||||
group('write', () {
|
||||
test('persists a value and reflects it in the composed view', () async {
|
||||
await sut.write(.themeMode, ThemeMode.dark);
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||
});
|
||||
|
||||
test('persists across domains independently', () async {
|
||||
await sut.write(.themeMode, ThemeMode.light);
|
||||
await sut.write(.logLevel, LogLevel.severe);
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.light);
|
||||
expect(sut.systemConfig.logLevel, LogLevel.severe);
|
||||
});
|
||||
});
|
||||
|
||||
group('delete', () {
|
||||
test('removes the row and reverts to default', () async {
|
||||
await sut.write(.themeMode, ThemeMode.dark);
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||
|
||||
await sut.delete(.themeMode);
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||
|
||||
final rows = await ctx.db.select(ctx.db.metadataEntity).get();
|
||||
expect(rows, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('refresh', () {
|
||||
test('picks up rows that were inserted directly into the DB', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.metadataEntity)
|
||||
.insert(
|
||||
MetadataEntityCompanion.insert(
|
||||
key: MetadataKey.themeMode.key,
|
||||
value: ThemeMode.dark.name,
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
// Cache hasn't seen this row yet — view still returns the default.
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||
|
||||
await MetadataRepository.refresh();
|
||||
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||
});
|
||||
|
||||
test('drops cached values for rows that were deleted out from under the repo', () async {
|
||||
await sut.write(.themeMode, ThemeMode.dark);
|
||||
// Wipe the row directly. Cache still holds the old value.
|
||||
await ctx.db.delete(ctx.db.metadataEntity).go();
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||
|
||||
await MetadataRepository.refresh();
|
||||
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||
});
|
||||
|
||||
test('skips rows whose key is unknown to MetadataKey', () async {
|
||||
await ctx.db
|
||||
.into(ctx.db.metadataEntity)
|
||||
.insert(
|
||||
MetadataEntityCompanion.insert(
|
||||
key: 'app-config.unknown.future-key',
|
||||
value: 'whatever',
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
|
||||
await MetadataRepository.refresh();
|
||||
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||
});
|
||||
});
|
||||
|
||||
group('watch', () {
|
||||
test('watchAppConfig emits the new value after a write', () async {
|
||||
final expectation = expectLater(sut.watchAppConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
|
||||
await sut.write(MetadataKey.themeMode, ThemeMode.dark);
|
||||
await expectation;
|
||||
});
|
||||
|
||||
test('watchAppConfig does not emit when only system-config rows change', () async {
|
||||
final emissions = <ThemeMode>[];
|
||||
// skip(1) drops the on-subscribe replay so we only capture emissions caused by the write below.
|
||||
final sub = sut.watchAppConfig().skip(1).listen((c) => emissions.add(c.theme.mode));
|
||||
|
||||
await sut.write(MetadataKey.logLevel, LogLevel.severe);
|
||||
await pumpEventQueue();
|
||||
await sub.cancel();
|
||||
|
||||
expect(emissions, isEmpty);
|
||||
});
|
||||
|
||||
test('watchSystemConfig emits the new value after a write', () async {
|
||||
final expectation = expectLater(sut.watchSystemConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
|
||||
await sut.write(MetadataKey.logLevel, LogLevel.warning);
|
||||
await expectation;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -109,36 +108,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('logout', () {
|
||||
test('Should logout user', () async {
|
||||
when(() => authApiRepository.logout()).thenAnswer((_) async => {});
|
||||
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
||||
when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null));
|
||||
when(
|
||||
() => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false),
|
||||
).thenAnswer((_) => Future.value(null));
|
||||
await sut.logout();
|
||||
|
||||
verify(() => authApiRepository.logout()).called(1);
|
||||
verify(() => backgroundSyncManager.cancel()).called(1);
|
||||
verify(() => authRepository.clearLocalData()).called(1);
|
||||
});
|
||||
|
||||
test('Should clear local data even on server error', () async {
|
||||
when(() => authApiRepository.logout()).thenThrow(Exception('Server error'));
|
||||
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
||||
when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null));
|
||||
when(
|
||||
() => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false),
|
||||
).thenAnswer((_) => Future.value(null));
|
||||
await sut.logout();
|
||||
|
||||
verify(() => authApiRepository.logout()).called(1);
|
||||
verify(() => backgroundSyncManager.cancel()).called(1);
|
||||
verify(() => authRepository.clearLocalData()).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('setOpenApiServiceEndpoint', () {
|
||||
setUp(() {
|
||||
when(() => networkService.getWifiName()).thenAnswer((_) async => 'TestWifi');
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
|
||||
void main() {
|
||||
group('MetadataKey', () {
|
||||
test('every key round-trips its default value losslessly', () {
|
||||
for (final key in MetadataKey.values) {
|
||||
final encoded = key.encode(key.defaultValue);
|
||||
final decoded = key.decode(encoded);
|
||||
expect(decoded, key.defaultValue, reason: 'round-trip failed for ${key.name}');
|
||||
}
|
||||
});
|
||||
|
||||
test('decode falls back to the default value when the raw input is unparseable', () {
|
||||
for (final key in MetadataKey.values) {
|
||||
expect(
|
||||
key.decode('not a valid encoding for any key'),
|
||||
key.defaultValue,
|
||||
reason: 'fallback failed for ${key.name}',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5172,7 +5172,7 @@
|
||||
},
|
||||
"/duplicates/{id}": {
|
||||
"delete": {
|
||||
"description": "Delete a single duplicate asset specified by its ID.",
|
||||
"description": "Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.",
|
||||
"operationId": "deleteDuplicate",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -5202,7 +5202,7 @@
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete a duplicate",
|
||||
"summary": "Dismiss a duplicate group",
|
||||
"tags": [
|
||||
"Duplicates"
|
||||
],
|
||||
@@ -14969,7 +14969,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.7.5",
|
||||
"version": "3.0.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
@@ -23052,118 +23052,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetOcrDeleteV1": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"description": "Original asset ID of the deleted OCR entry",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"description": "Timestamp when the OCR entry was deleted",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Audit row ID of the deleted OCR entry",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"deletedAt",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetOcrV1": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"description": "Asset ID",
|
||||
"type": "string"
|
||||
},
|
||||
"boxScore": {
|
||||
"description": "Confidence score of the bounding box",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"description": "OCR entry ID",
|
||||
"type": "string"
|
||||
},
|
||||
"isVisible": {
|
||||
"description": "Whether the OCR entry is visible",
|
||||
"type": "boolean"
|
||||
},
|
||||
"text": {
|
||||
"description": "Recognized text content",
|
||||
"type": "string"
|
||||
},
|
||||
"textScore": {
|
||||
"description": "Confidence score of the recognized text",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"x1": {
|
||||
"description": "Top-left X coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"x2": {
|
||||
"description": "Top-right X coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"x3": {
|
||||
"description": "Bottom-right X coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"x4": {
|
||||
"description": "Bottom-left X coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"y1": {
|
||||
"description": "Top-left Y coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"y2": {
|
||||
"description": "Top-right Y coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"y3": {
|
||||
"description": "Bottom-right Y coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"y4": {
|
||||
"description": "Bottom-left Y coordinate (normalized 0–1)",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetId",
|
||||
"boxScore",
|
||||
"id",
|
||||
"isVisible",
|
||||
"text",
|
||||
"textScore",
|
||||
"x1",
|
||||
"x2",
|
||||
"x3",
|
||||
"x4",
|
||||
"y1",
|
||||
"y2",
|
||||
"y3",
|
||||
"y4"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetV1": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
@@ -23527,8 +23415,6 @@
|
||||
"AssetEditDeleteV1",
|
||||
"AssetMetadataV1",
|
||||
"AssetMetadataDeleteV1",
|
||||
"AssetOcrV1",
|
||||
"AssetOcrDeleteV1",
|
||||
"PartnerV1",
|
||||
"PartnerDeleteV1",
|
||||
"PartnerAssetV1",
|
||||
@@ -23851,7 +23737,6 @@
|
||||
"AssetExifsV1",
|
||||
"AssetEditsV1",
|
||||
"AssetMetadataV1",
|
||||
"AssetOcrV1",
|
||||
"AuthUsersV1",
|
||||
"MemoriesV1",
|
||||
"MemoryToAssetsV1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.7.5
|
||||
* 3.0.0
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -3037,44 +3037,6 @@ export type SyncAssetMetadataV1 = {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export type SyncAssetOcrDeleteV1 = {
|
||||
/** Original asset ID of the deleted OCR entry */
|
||||
assetId: string;
|
||||
/** Timestamp when the OCR entry was deleted */
|
||||
deletedAt: string;
|
||||
/** Audit row ID of the deleted OCR entry */
|
||||
id: string;
|
||||
};
|
||||
export type SyncAssetOcrV1 = {
|
||||
/** Asset ID */
|
||||
assetId: string;
|
||||
/** Confidence score of the bounding box */
|
||||
boxScore: number;
|
||||
/** OCR entry ID */
|
||||
id: string;
|
||||
/** Whether the OCR entry is visible */
|
||||
isVisible: boolean;
|
||||
/** Recognized text content */
|
||||
text: string;
|
||||
/** Confidence score of the recognized text */
|
||||
textScore: number;
|
||||
/** Top-left X coordinate (normalized 0–1) */
|
||||
x1: number;
|
||||
/** Top-right X coordinate (normalized 0–1) */
|
||||
x2: number;
|
||||
/** Bottom-right X coordinate (normalized 0–1) */
|
||||
x3: number;
|
||||
/** Bottom-left X coordinate (normalized 0–1) */
|
||||
x4: number;
|
||||
/** Top-left Y coordinate (normalized 0–1) */
|
||||
y1: number;
|
||||
/** Top-right Y coordinate (normalized 0–1) */
|
||||
y2: number;
|
||||
/** Bottom-right Y coordinate (normalized 0–1) */
|
||||
y3: number;
|
||||
/** Bottom-left Y coordinate (normalized 0–1) */
|
||||
y4: number;
|
||||
};
|
||||
export type SyncAssetV1 = {
|
||||
/** Checksum */
|
||||
checksum: string;
|
||||
@@ -4518,7 +4480,7 @@ export function resolveDuplicates({ duplicateResolveDto }: {
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete a duplicate
|
||||
* Dismiss a duplicate group
|
||||
*/
|
||||
export function deleteDuplicate({ id }: {
|
||||
id: string;
|
||||
@@ -7192,8 +7154,6 @@ export enum SyncEntityType {
|
||||
AssetEditDeleteV1 = "AssetEditDeleteV1",
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
AssetMetadataDeleteV1 = "AssetMetadataDeleteV1",
|
||||
AssetOcrV1 = "AssetOcrV1",
|
||||
AssetOcrDeleteV1 = "AssetOcrDeleteV1",
|
||||
PartnerV1 = "PartnerV1",
|
||||
PartnerDeleteV1 = "PartnerDeleteV1",
|
||||
PartnerAssetV1 = "PartnerAssetV1",
|
||||
@@ -7254,7 +7214,6 @@ export enum SyncRequestType {
|
||||
AssetExifsV1 = "AssetExifsV1",
|
||||
AssetEditsV1 = "AssetEditsV1",
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
AssetOcrV1 = "AssetOcrV1",
|
||||
AuthUsersV1 = "AuthUsersV1",
|
||||
MemoriesV1 = "MemoriesV1",
|
||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.7.5",
|
||||
"version": "3.0.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -41,8 +41,8 @@ export class DuplicateController {
|
||||
@Authenticated({ permission: Permission.DuplicateDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Delete a duplicate',
|
||||
description: 'Delete a single duplicate asset specified by its ID.',
|
||||
summary: 'Dismiss a duplicate group',
|
||||
description: 'Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { VideoInterfaces } from 'src/types';
|
||||
import { getAssetFile } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
|
||||
@@ -299,6 +300,11 @@ export class StorageCore {
|
||||
return this.storageRepository.removeEmptyDirs(StorageCore.getBaseFolder(folder));
|
||||
}
|
||||
|
||||
async getVideoInterfaces(): Promise<VideoInterfaces> {
|
||||
const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]);
|
||||
return { dri, mali };
|
||||
}
|
||||
|
||||
private savePath(pathType: PathType, id: string, newPath: string) {
|
||||
switch (pathType) {
|
||||
case AssetPathType.Original: {
|
||||
@@ -330,4 +336,26 @@ export class StorageCore {
|
||||
static getTempPathInDir(dir: string): string {
|
||||
return join(dir, `${randomUUID()}.tmp`);
|
||||
}
|
||||
|
||||
private async getDevices() {
|
||||
try {
|
||||
return await this.storageRepository.readdir('/dev/dri');
|
||||
} catch {
|
||||
this.logger.debug('No devices found in /dev/dri.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async hasMaliOpenCL() {
|
||||
try {
|
||||
const [maliIcdStat, maliDeviceStat] = await Promise.all([
|
||||
this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'),
|
||||
this.storageRepository.stat('/dev/mali0'),
|
||||
]);
|
||||
return maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
||||
} catch {
|
||||
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,23 +426,6 @@ export const columns = {
|
||||
'asset_exif.rating',
|
||||
'asset_exif.fps',
|
||||
],
|
||||
syncAssetOcr: [
|
||||
'asset_ocr.id',
|
||||
'asset_ocr.assetId',
|
||||
'asset_ocr.x1',
|
||||
'asset_ocr.y1',
|
||||
'asset_ocr.x2',
|
||||
'asset_ocr.y2',
|
||||
'asset_ocr.x3',
|
||||
'asset_ocr.y3',
|
||||
'asset_ocr.x4',
|
||||
'asset_ocr.y4',
|
||||
'asset_ocr.text',
|
||||
'asset_ocr.boxScore',
|
||||
'asset_ocr.textScore',
|
||||
'asset_ocr.updateId',
|
||||
'asset_ocr.isVisible',
|
||||
],
|
||||
syncAssetEdit: [
|
||||
'asset_edit.id',
|
||||
'asset_edit.assetId',
|
||||
|
||||
@@ -408,41 +408,6 @@ class SyncMemoryDeleteV1 extends createZodDto(SyncMemoryDeleteV1Schema) {}
|
||||
class SyncMemoryAssetV1 extends createZodDto(SyncMemoryAssetV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncMemoryAssetDeleteV1 extends createZodDto(SyncMemoryAssetDeleteV1Schema) {}
|
||||
|
||||
const SyncAssetOcrV1Schema = z
|
||||
.object({
|
||||
id: z.string().describe('OCR entry ID'),
|
||||
assetId: z.string().describe('Asset ID'),
|
||||
|
||||
x1: z.number().meta({ format: 'double' }).describe('Top-left X coordinate (normalized 0–1)'),
|
||||
y1: z.number().meta({ format: 'double' }).describe('Top-left Y coordinate (normalized 0–1)'),
|
||||
x2: z.number().meta({ format: 'double' }).describe('Top-right X coordinate (normalized 0–1)'),
|
||||
y2: z.number().meta({ format: 'double' }).describe('Top-right Y coordinate (normalized 0–1)'),
|
||||
x3: z.number().meta({ format: 'double' }).describe('Bottom-right X coordinate (normalized 0–1)'),
|
||||
y3: z.number().meta({ format: 'double' }).describe('Bottom-right Y coordinate (normalized 0–1)'),
|
||||
x4: z.number().meta({ format: 'double' }).describe('Bottom-left X coordinate (normalized 0–1)'),
|
||||
y4: z.number().meta({ format: 'double' }).describe('Bottom-left Y coordinate (normalized 0–1)'),
|
||||
|
||||
boxScore: z.number().meta({ format: 'double' }).describe('Confidence score of the bounding box'),
|
||||
textScore: z.number().meta({ format: 'double' }).describe('Confidence score of the recognized text'),
|
||||
text: z.string().describe('Recognized text content'),
|
||||
isVisible: z.boolean().describe('Whether the OCR entry is visible'),
|
||||
})
|
||||
.meta({ id: 'SyncAssetOcrV1' });
|
||||
|
||||
const SyncAssetOcrDeleteV1Schema = z
|
||||
.object({
|
||||
id: z.string().describe('Audit row ID of the deleted OCR entry'),
|
||||
assetId: z.string().describe('Original asset ID of the deleted OCR entry'),
|
||||
deletedAt: isoDatetimeToDate.describe('Timestamp when the OCR entry was deleted'),
|
||||
})
|
||||
.meta({ id: 'SyncAssetOcrDeleteV1' });
|
||||
|
||||
@ExtraModel()
|
||||
class SyncAssetOcrV1 extends createZodDto(SyncAssetOcrV1Schema) {}
|
||||
|
||||
@ExtraModel()
|
||||
class SyncAssetOcrDeleteV1 extends createZodDto(SyncAssetOcrDeleteV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncStackV1 extends createZodDto(SyncStackV1Schema) {}
|
||||
@ExtraModel()
|
||||
@@ -479,8 +444,6 @@ export type SyncItem = {
|
||||
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
|
||||
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
|
||||
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AssetOcrV1]: SyncAssetOcrV1;
|
||||
[SyncEntityType.AssetOcrDeleteV1]: SyncAssetOcrDeleteV1;
|
||||
[SyncEntityType.AssetEditV1]: SyncAssetEditV1;
|
||||
[SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1;
|
||||
[SyncEntityType.PartnerAssetV2]: SyncAssetV2;
|
||||
|
||||
@@ -943,7 +943,6 @@ export enum SyncRequestType {
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AssetEditsV1 = 'AssetEditsV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetOcrV1 = 'AssetOcrV1',
|
||||
AuthUsersV1 = 'AuthUsersV1',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
@@ -982,8 +981,6 @@ export enum SyncEntityType {
|
||||
AssetEditDeleteV1 = 'AssetEditDeleteV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1',
|
||||
AssetOcrV1 = 'AssetOcrV1',
|
||||
AssetOcrDeleteV1 = 'AssetOcrDeleteV1',
|
||||
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
@@ -575,48 +575,6 @@ where
|
||||
order by
|
||||
"asset_metadata"."updateId" asc
|
||||
|
||||
-- SyncRepository.assetOcr.getDeletes
|
||||
select
|
||||
"asset_ocr_audit"."id",
|
||||
"asset_ocr_audit"."assetId",
|
||||
"asset_ocr_audit"."deletedAt"
|
||||
from
|
||||
"asset_ocr_audit" as "asset_ocr_audit"
|
||||
left join "asset" on "asset"."id" = "asset_ocr_audit"."assetId"
|
||||
where
|
||||
"asset_ocr_audit"."id" < $1
|
||||
and "asset_ocr_audit"."id" > $2
|
||||
and "asset"."ownerId" = $3
|
||||
order by
|
||||
"asset_ocr_audit"."id" asc
|
||||
|
||||
-- SyncRepository.assetOcr.getUpserts
|
||||
select
|
||||
"asset_ocr"."id",
|
||||
"asset_ocr"."assetId",
|
||||
"asset_ocr"."x1",
|
||||
"asset_ocr"."y1",
|
||||
"asset_ocr"."x2",
|
||||
"asset_ocr"."y2",
|
||||
"asset_ocr"."x3",
|
||||
"asset_ocr"."y3",
|
||||
"asset_ocr"."x4",
|
||||
"asset_ocr"."y4",
|
||||
"asset_ocr"."text",
|
||||
"asset_ocr"."boxScore",
|
||||
"asset_ocr"."textScore",
|
||||
"asset_ocr"."updateId",
|
||||
"asset_ocr"."isVisible"
|
||||
from
|
||||
"asset_ocr" as "asset_ocr"
|
||||
inner join "asset" on "asset"."id" = "asset_ocr"."assetId"
|
||||
where
|
||||
"asset_ocr"."updateId" < $1
|
||||
and "asset_ocr"."updateId" > $2
|
||||
and "asset"."ownerId" = $3
|
||||
order by
|
||||
"asset_ocr"."updateId" asc
|
||||
|
||||
-- SyncRepository.authUser.getUpserts
|
||||
select
|
||||
"id",
|
||||
|
||||
@@ -71,7 +71,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
describe('applyEdits (single actions)', () => {
|
||||
it('should apply crop edit correctly', async () => {
|
||||
const result = await sut['applyEdits'](
|
||||
const result = sut['applyEdits'](
|
||||
sharp({
|
||||
create: {
|
||||
width: 1000,
|
||||
@@ -98,7 +98,7 @@ describe(MediaRepository.name, () => {
|
||||
expect(metadata.height).toBe(300);
|
||||
});
|
||||
it('should apply rotate edit correctly', async () => {
|
||||
const result = await sut['applyEdits'](
|
||||
const result = sut['applyEdits'](
|
||||
sharp({
|
||||
create: {
|
||||
width: 500,
|
||||
@@ -123,7 +123,7 @@ describe(MediaRepository.name, () => {
|
||||
});
|
||||
|
||||
it('should apply mirror edit correctly', async () => {
|
||||
const resultHorizontal = await sut['applyEdits'](sharp(await buildTestQuadImage()), [
|
||||
const resultHorizontal = sut['applyEdits'](sharp(await buildTestQuadImage()), [
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
@@ -142,7 +142,7 @@ describe(MediaRepository.name, () => {
|
||||
expect(await getPixelColor(bufferHorizontal, 10, 990)).toEqual({ r: 255, g: 255, b: 0 });
|
||||
expect(await getPixelColor(bufferHorizontal, 990, 990)).toEqual({ r: 0, g: 0, b: 255 });
|
||||
|
||||
const resultVertical = await sut['applyEdits'](sharp(await buildTestQuadImage()), [
|
||||
const resultVertical = sut['applyEdits'](sharp(await buildTestQuadImage()), [
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
@@ -170,7 +170,7 @@ describe(MediaRepository.name, () => {
|
||||
describe('applyEdits (multiple sequential edits)', () => {
|
||||
it('should apply horizontal mirror then vertical mirror (equivalent to 180° rotation)', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } },
|
||||
]);
|
||||
@@ -188,7 +188,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply rotate 90° then horizontal mirror', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||
]);
|
||||
@@ -206,7 +206,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply 180° rotation', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 180 } },
|
||||
]);
|
||||
|
||||
@@ -223,7 +223,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply 270° rotations', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 270 } },
|
||||
]);
|
||||
|
||||
@@ -240,7 +240,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply crop then rotate 90°', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 1000, height: 500 } },
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||
]);
|
||||
@@ -256,7 +256,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply rotate 90° then crop', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 1000 } },
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||
]);
|
||||
@@ -272,7 +272,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply vertical mirror then horizontal mirror then rotate 90°', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } },
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||
@@ -291,7 +291,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply crop to single quadrant then mirror', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 500 } },
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||
]);
|
||||
@@ -309,7 +309,7 @@ describe(MediaRepository.name, () => {
|
||||
|
||||
it('should apply all operations: crop, rotate, mirror', async () => {
|
||||
const imageBuffer = await buildTestQuadImage();
|
||||
const result = await sut['applyEdits'](sharp(imageBuffer), [
|
||||
const result = sut['applyEdits'](sharp(imageBuffer), [
|
||||
{ action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 1000 } },
|
||||
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||
|
||||
@@ -77,34 +77,20 @@ export class MediaRepository {
|
||||
* @returns ExtractResult if succeeded, or null if failed
|
||||
*/
|
||||
async extract(input: string): Promise<ExtractResult | null> {
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
||||
return { buffer, format: RawExtractedFormat.Jxl };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract preview buffer from image: ${error}`);
|
||||
return null;
|
||||
for (const { tag, format } of [
|
||||
{ tag: 'JpgFromRaw2', format: RawExtractedFormat.Jpeg },
|
||||
{ tag: 'JpgFromRaw', format: RawExtractedFormat.Jpeg },
|
||||
{ tag: 'PreviewJXL', format: RawExtractedFormat.Jxl },
|
||||
{ tag: 'PreviewImage', format: RawExtractedFormat.Jpeg },
|
||||
]) {
|
||||
try {
|
||||
const buffer = await exiftool.extractBinaryTagToBuffer(tag, input);
|
||||
return { buffer, format };
|
||||
} catch (error: any) {
|
||||
this.logger.debug(`Could not extract ${tag} buffer from image: ${error}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
||||
@@ -162,49 +148,45 @@ export class MediaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||
const pipeline = await this.getImageDecodingPipeline(input, options);
|
||||
return pipeline.raw().toBuffer({ resolveWithObject: true });
|
||||
decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||
return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
|
||||
}
|
||||
|
||||
private async applyEdits(pipeline: sharp.Sharp, edits: AssetEditActionItem[]): Promise<sharp.Sharp> {
|
||||
const affineEditOperations = edits.filter((edit) => edit.action !== 'crop');
|
||||
const matrix = createAffineMatrix(affineEditOperations);
|
||||
|
||||
private applyEdits(pipeline: sharp.Sharp, edits: AssetEditActionItem[]): sharp.Sharp {
|
||||
const crop = edits.find((edit) => edit.action === 'crop');
|
||||
const dimensions = await pipeline.metadata();
|
||||
|
||||
if (crop) {
|
||||
pipeline = pipeline.extract({
|
||||
left: crop ? Math.round(crop.parameters.x) : 0,
|
||||
top: crop ? Math.round(crop.parameters.y) : 0,
|
||||
width: crop ? Math.round(crop.parameters.width) : dimensions.width || 0,
|
||||
height: crop ? Math.round(crop.parameters.height) : dimensions.height || 0,
|
||||
left: Math.round(crop.parameters.x),
|
||||
top: Math.round(crop.parameters.y),
|
||||
width: Math.round(crop.parameters.width),
|
||||
height: Math.round(crop.parameters.height),
|
||||
});
|
||||
}
|
||||
|
||||
const { a, b, c, d } = matrix;
|
||||
pipeline = pipeline.affine([
|
||||
[a, b],
|
||||
[c, d],
|
||||
]);
|
||||
const affineEditOperations = edits.filter((edit) => edit.action !== 'crop');
|
||||
if (affineEditOperations.length > 0) {
|
||||
const { a, b, c, d } = createAffineMatrix(affineEditOperations);
|
||||
pipeline = pipeline.affine([
|
||||
[a, b],
|
||||
[c, d],
|
||||
]);
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
|
||||
const pipeline = await this.getImageDecodingPipeline(input, options);
|
||||
const decoded = pipeline.toFormat(options.format, {
|
||||
quality: options.quality,
|
||||
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
|
||||
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
|
||||
progressive: options.progressive,
|
||||
});
|
||||
|
||||
await decoded.toFile(output);
|
||||
await this.getImageDecodingPipeline(input, options)
|
||||
.toFormat(options.format, {
|
||||
quality: options.quality,
|
||||
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
|
||||
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
|
||||
progressive: options.progressive,
|
||||
})
|
||||
.toFile(output);
|
||||
}
|
||||
|
||||
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||
private getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||
let pipeline = sharp(input, {
|
||||
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
|
||||
failOn: options.processInvalidImages ? 'none' : 'error',
|
||||
@@ -228,7 +210,7 @@ export class MediaRepository {
|
||||
}
|
||||
|
||||
if (options.edits && options.edits.length > 0) {
|
||||
pipeline = await this.applyEdits(pipeline, options.edits);
|
||||
pipeline = this.applyEdits(pipeline, options.edits);
|
||||
}
|
||||
|
||||
if (options.size !== undefined) {
|
||||
@@ -238,19 +220,18 @@ export class MediaRepository {
|
||||
}
|
||||
|
||||
async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise<Buffer> {
|
||||
const [{ rgbaToThumbHash }, decodingPipeline] = await Promise.all([
|
||||
import('thumbhash'),
|
||||
this.getImageDecodingPipeline(input, {
|
||||
colorspace: options.colorspace,
|
||||
processInvalidImages: options.processInvalidImages,
|
||||
raw: options.raw,
|
||||
edits: options.edits,
|
||||
}),
|
||||
]);
|
||||
const { rgbaToThumbHash } = await import('thumbhash');
|
||||
|
||||
const pipeline = decodingPipeline.resize(100, 100, { fit: 'inside', withoutEnlargement: true }).raw().ensureAlpha();
|
||||
|
||||
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
||||
const { data, info } = await this.getImageDecodingPipeline(input, {
|
||||
colorspace: options.colorspace,
|
||||
processInvalidImages: options.processInvalidImages,
|
||||
raw: options.raw,
|
||||
edits: options.edits,
|
||||
})
|
||||
.resize(100, 100, { fit: 'inside', withoutEnlargement: true })
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
return Buffer.from(rgbaToThumbHash(info.width, info.height, data));
|
||||
}
|
||||
@@ -274,23 +255,23 @@ export class MediaRepository {
|
||||
index: stream.index,
|
||||
height,
|
||||
width: dar ? Math.round(height * dar) : this.parseInt(stream.width),
|
||||
codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
|
||||
profile: this.parseVideoProfile(stream.codec_name, stream.profile as string | undefined),
|
||||
codecName: stream.codec_name === 'h265' ? 'hevc' : (stream.codec_name ?? null),
|
||||
profile: this.parseVideoProfile(stream.codec_name, stream.profile as string | undefined) ?? null,
|
||||
level: this.parseOptionalInt(stream.level),
|
||||
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
|
||||
frameRate: this.parseFrameRate(stream.avg_frame_rate ?? stream.r_frame_rate),
|
||||
timeBase: this.parseRational(stream.time_base)?.den,
|
||||
timeBase: this.parseRational(stream.time_base)?.den ?? null,
|
||||
rotation: this.parseInt(stream.rotation),
|
||||
bitrate: this.parseInt(stream.bit_rate),
|
||||
pixelFormat: stream.pix_fmt || 'yuv420p',
|
||||
colorPrimaries: this.parseEnum(ColorPrimaries, stream.color_primaries) ?? ColorPrimaries.Unknown,
|
||||
colorMatrix: this.parseEnum(ColorMatrix, stream.color_space) ?? ColorMatrix.Unknown,
|
||||
colorTransfer: this.parseEnum(ColorTransfer, stream.color_transfer) ?? ColorTransfer.Unknown,
|
||||
dvProfile: this.parseOptionalInt(stream.dv_profile) as DvProfile | undefined,
|
||||
dvProfile: this.parseOptionalInt(stream.dv_profile) as DvProfile | null,
|
||||
dvLevel: this.parseOptionalInt(stream.dv_level),
|
||||
dvBlSignalCompatibilityId: this.parseOptionalInt(stream.dv_bl_signal_compatibility_id) as
|
||||
| DvSignalCompatibility
|
||||
| undefined,
|
||||
dvBlSignalCompatibilityId: this.parseOptionalInt(
|
||||
stream.dv_bl_signal_compatibility_id,
|
||||
) as DvSignalCompatibility | null,
|
||||
};
|
||||
}),
|
||||
audioStreams: results.streams
|
||||
@@ -298,9 +279,9 @@ export class MediaRepository {
|
||||
.sort((a, b) => this.compareStreams(a, b))
|
||||
.map((stream) => ({
|
||||
index: stream.index,
|
||||
codecName: stream.codec_name,
|
||||
codecName: stream.codec_name ?? null,
|
||||
profile:
|
||||
stream.codec_name === 'aac' ? this.parseEnum(AacProfile, stream.profile as string | undefined) : undefined,
|
||||
stream.codec_name === 'aac' ? this.parseEnum(AacProfile, stream.profile as string | undefined) : null,
|
||||
bitrate: this.parseInt(stream.bit_rate),
|
||||
})),
|
||||
};
|
||||
@@ -449,29 +430,29 @@ export class MediaRepository {
|
||||
return Number.parseFloat(value as string) || 0;
|
||||
}
|
||||
|
||||
private parseOptionalInt(value: string | number | undefined): number | undefined {
|
||||
private parseOptionalInt(value: string | number | undefined): number | null {
|
||||
const parsed = Number.parseInt(value as string);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
private parseEnum<E extends Record<string, number | string>>(enumObj: E, value?: string) {
|
||||
return value ? (enumObj[pascalCase(value)] as Extract<E[keyof E], number> | undefined) : undefined;
|
||||
return value ? ((enumObj[pascalCase(value)] as Extract<E[keyof E], number> | undefined) ?? null) : null;
|
||||
}
|
||||
|
||||
/** Parse a rational like "60000/1001" or "1/600" into `{ num, den }`. */
|
||||
private parseRational(value: string | undefined): { num: number; den: number } | undefined {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const [num, den = 1] = value.split('/').map(Number);
|
||||
if (num && den) {
|
||||
return { num, den };
|
||||
private parseRational(value: string | undefined): { num: number; den: number } | null {
|
||||
if (value) {
|
||||
const [num, den = 1] = value.split('/').map(Number);
|
||||
if (num && den) {
|
||||
return { num, den };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private parseFrameRate(value: string | undefined): number | undefined {
|
||||
private parseFrameRate(value: string | undefined): number | null {
|
||||
const r = this.parseRational(value);
|
||||
return r ? r.num / r.den : undefined;
|
||||
return r ? r.num / r.den : null;
|
||||
}
|
||||
|
||||
private getDar(dar: string | undefined): number {
|
||||
@@ -498,6 +479,7 @@ export class MediaRepository {
|
||||
return this.parseEnum(Av1Profile, profile);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private compareStreams(a: FfprobeStream, b: FfprobeStream): number {
|
||||
|
||||
@@ -56,7 +56,6 @@ export class SyncRepository {
|
||||
assetEdit: AssetEditSync;
|
||||
assetFace: AssetFaceSync;
|
||||
assetMetadata: AssetMetadataSync;
|
||||
assetOcr: AssetOcrSync;
|
||||
authUser: AuthUserSync;
|
||||
memory: MemorySync;
|
||||
memoryToAsset: MemoryToAssetSync;
|
||||
@@ -80,7 +79,6 @@ export class SyncRepository {
|
||||
this.assetEdit = new AssetEditSync(this.db);
|
||||
this.assetFace = new AssetFaceSync(this.db);
|
||||
this.assetMetadata = new AssetMetadataSync(this.db);
|
||||
this.assetOcr = new AssetOcrSync(this.db);
|
||||
this.authUser = new AuthUserSync(this.db);
|
||||
this.memory = new MemorySync(this.db);
|
||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||
@@ -769,27 +767,3 @@ class AssetMetadataSync extends BaseSync {
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AssetOcrSync extends BaseSync {
|
||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||
getDeletes(options: SyncQueryOptions, userId: string) {
|
||||
return this.auditQuery('asset_ocr_audit', options)
|
||||
.select(['asset_ocr_audit.id', 'asset_ocr_audit.assetId', 'asset_ocr_audit.deletedAt'])
|
||||
.leftJoin('asset', 'asset.id', 'asset_ocr_audit.assetId')
|
||||
.where('asset.ownerId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
|
||||
cleanupAuditTable(daysAgo: number) {
|
||||
return this.auditCleanup('asset_ocr_audit', daysAgo);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
|
||||
getUpserts(options: SyncQueryOptions, userId: string) {
|
||||
return this.upsertQuery('asset_ocr', options)
|
||||
.select(columns.syncAssetOcr)
|
||||
.innerJoin('asset', 'asset.id', 'asset_ocr.assetId')
|
||||
.where('asset.ownerId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,16 +287,3 @@ export const asset_edit_audit = registerFunction({
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
export const asset_ocr_delete_audit = registerFunction({
|
||||
name: 'asset_ocr_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO asset_ocr_audit ("assetId")
|
||||
SELECT "assetId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user