Compare commits

..

1 Commits

Author SHA1 Message Date
shenlong-tanwen
ff7d92b380 refactor: auto trash / restore sync 2026-07-15 06:20:48 +05:30
200 changed files with 21439 additions and 6409 deletions

View File

@@ -51,11 +51,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
@@ -80,12 +79,10 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -205,11 +202,10 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer

View File

@@ -19,12 +19,10 @@ jobs:
actions: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-actions: write
- name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -31,11 +31,10 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -64,11 +63,10 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -44,11 +44,10 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -23,11 +23,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check

View File

@@ -21,11 +21,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
@@ -55,11 +54,10 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -20,12 +20,10 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-actions: read
permission-pull-requests: read
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
run: echo 'The triggering workflow did not succeed' && exit 1
@@ -128,13 +126,10 @@ 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@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-actions: read
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -17,12 +17,10 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -15,12 +15,10 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -35,8 +35,6 @@ jobs:
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Find translation PR
id: find_pr

View File

@@ -14,11 +14,10 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5.6.0

View File

@@ -12,12 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with:

View File

@@ -54,11 +54,10 @@ jobs:
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -136,8 +135,6 @@ jobs:
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-actions: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -14,11 +14,10 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
with:
@@ -33,11 +32,10 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:

View File

@@ -16,11 +16,10 @@ jobs:
packages: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -20,11 +20,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
@@ -34,7 +33,6 @@ jobs:
filters: |
mobile:
- 'mobile/**'
- 'i18n/en.json'
force-filters: |
- '.github/workflows/static_analysis.yml'
force-events: 'workflow_dispatch,release'
@@ -51,11 +49,10 @@ jobs:
working-directory: ./mobile
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -17,11 +17,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
@@ -78,11 +77,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -107,11 +105,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -139,11 +136,10 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -171,11 +167,10 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -216,11 +211,10 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -255,11 +249,10 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -284,11 +277,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -335,11 +327,10 @@ jobs:
working-directory: ./e2e
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -368,11 +359,10 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -405,11 +395,10 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -483,11 +472,10 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -592,11 +580,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -631,11 +618,10 @@ jobs:
working-directory: ./machine-learning
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -662,11 +648,10 @@ jobs:
working-directory: ./.github
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -693,11 +678,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -715,11 +699,10 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -774,11 +757,10 @@ jobs:
- 5432:5432
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

View File

@@ -24,11 +24,10 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
@@ -48,11 +47,10 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: read
- name: Bot review status
env:

View File

@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893
image: prom/prometheus@sha256:a75c5a35bc21d7afe69551eefa3cb1e1fb1775fe759408007a66b54ec3de1f29
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus

View File

@@ -99,21 +99,6 @@ To see local changes to `@immich/ui` in Immich, do the following:
2. Run `mise //mobile:translation` to generate the translation file.
3. Change to the `mobile/` directory and run `flutter run` to start the app.
##### iOS Code Signing
The Immich Apple Team ID and bundle IDs are specified in `mobile/ios/Signing.xcconfig`. For local development, we provide an override mechanism.
Create `mobile/ios/Signing.local.xcconfig` and populate it with the necessary values needed to build and sign Immich yourself. This local override file is gitignored.
```
IMMICH_TEAM_ID = ABCDE12345
IMMICH_BUNDLE_ID_PROD = com.customuniqueid.immich
IMMICH_BUNDLE_ID_DEV = com.customuniqueid.immichdev
IMMICH_GROUP_ID = group.com.customuniqueid.immich
```
The environment values are used across Immich's targets and schemes to prevent redundant edits by contributors.
#### Translation
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then run:

View File

@@ -99,7 +99,6 @@ Options:
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
--visibility <visibility> Set the visibility of uploaded assets (choices: "archive", "timeline", "hidden", "locked", env: IMMICH_VISIBILITY)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
@@ -177,12 +176,6 @@ By default, hidden files are skipped. If you want to include hidden files, use t
immich upload --include-hidden --recursive directory/
```
You can set the visibility of uploaded assets to `archive`, `timeline`, `hidden`, or `locked` with the `--visibility` option:
```bash
immich upload --visibility archive --recursive directory/
```
You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
output you will need to strip the first three lines of output to get the json.

View File

@@ -121,7 +121,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
width="90%"
alt="Go to Docker Tab and visit the address listed next to immich-web"
/>
</details>
:::tip

View File

@@ -1,7 +1,7 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.wrangler]]
version = "4.110.0"
version = "4.100.0"
backend = "npm:wrangler"
[tools.wrangler.options]

View File

@@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.110.0"
wrangler = "4.100.0"

View File

@@ -32,7 +32,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.13.3",
"@types/node": "^24.13.2",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0",

View File

@@ -814,6 +814,7 @@
"custom_date": "Custom date",
"custom_locale": "Custom locale",
"custom_locale_description": "Format dates, times, and numbers based on the selected language and region",
"custom_url": "Custom URL",
"cutoff_date_description": "Keep photos from the last…",
"cutoff_day": "{count, plural, one {day} other {days}}",
"cutoff_year": "{count, plural, one {year} other {years}}",
@@ -1947,9 +1948,7 @@
"shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_create_error": "Error while creating shared link",
"shared_link_custom_url_description": "Access this shared link with a custom URL name",
"shared_link_custom_url_title": "Custom URL",
"shared_link_custom_url_warning": "Warning: a custom URL name with a forward slash may not behave as you expect.",
"shared_link_custom_url_description": "Access this shared link with a custom URL",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{count} days",

View File

@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
[[tools.pnpm]]
version = "11.11.0"
version = "11.6.0"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:4871093439f036b3082df6102255ac119f3b6f4f50c51c5711fc7c358b8b22d7"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734816"
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:f97316cee0deb3ae3fd64e8a09e1c8c88a76da33373a2b465bd6a4c3e85e0690"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734819"
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:df4699e897012ab14df2cc6eaa942910e830eb7fcaa420a2a1421a9461fd9108"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734818"
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:aad186f8a8ae72d827d5efe63e99e801456715fb7f2798949405fa33f20260db"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734821"
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:ad46ad16c2376c2b78354ac488c1b166e076aa59bfcbcfd567d55957d755a690"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734814"
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:13ad8a9b139c4c829fea33462cf04d061d72f0926b2b76a92ac03c9a36e405f9"
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734820"
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
provenance = "github-attestations"
[[tools.terragrunt]]

View File

@@ -16,7 +16,7 @@ config_roots = [
[tools]
node = "24.15.0"
pnpm = "11.11.0"
pnpm = "11.6.0"
terragrunt = "1.0.3"
opentofu = "1.11.6"
"npm:oazapfts" = "7.5.0"

View File

@@ -89,6 +89,13 @@ flutter {
}
dependencies {
constraints {
implementation("androidx.glance:glance-appwidget") {
version { strictly libs.versions.glance.get() }
because 'home_widget requests 1.+ which can resolve to pre-releases incompatible with our compileSdk/AGP'
}
}
implementation libs.okhttp
implementation libs.cronet.embedded
implementation libs.media3.datasource.okhttp

View File

@@ -18,6 +18,7 @@ import app.alextran.immich.images.LocalImageApi
import app.alextran.immich.images.LocalImagesImpl
import app.alextran.immich.images.RemoteImageApi
import app.alextran.immich.images.RemoteImagesImpl
import app.alextran.immich.media.AssetMediaApiImpl
import app.alextran.immich.permission.PermissionApi
import app.alextran.immich.permission.PermissionApiImpl
import app.alextran.immich.sync.NativeSyncApi
@@ -66,6 +67,7 @@ class MainActivity : FlutterFragmentActivity() {
flutterEngine.plugins.add(backgroundEngineLockImpl)
flutterEngine.plugins.add(nativeSyncApiImpl)
flutterEngine.plugins.add(permissionApiImpl)
flutterEngine.plugins.add(AssetMediaApiImpl(ctx))
}
fun cancelPlugins(flutterEngine: FlutterEngine) {
@@ -75,6 +77,8 @@ class MainActivity : FlutterFragmentActivity() {
nativeApi?.detachFromEngine()
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
permissionApi?.detachFromEngine()
val assetMediaApi = flutterEngine.plugins.get(AssetMediaApiImpl::class.java) as ImmichPlugin?
assetMediaApi?.detachFromEngine()
}
}
}

View File

@@ -2,17 +2,30 @@ package app.alextran.immich.core
import androidx.annotation.CallSuper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.isActive
abstract class ImmichPlugin : FlutterPlugin {
private var detached: Boolean = false;
private var detached: Boolean = false
private var supervisor = SupervisorJob()
protected var scope = CoroutineScope(supervisor + Dispatchers.Default)
private set
@CallSuper
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
detached = false;
detached = false
if (!scope.isActive) {
supervisor = SupervisorJob()
scope = CoroutineScope(supervisor + Dispatchers.Default)
}
}
fun detachFromEngine() {
detached = true
supervisor.cancel()
}
@CallSuper
@@ -22,8 +35,9 @@ abstract class ImmichPlugin : FlutterPlugin {
fun <T> completeWhenActive(callback: (T) -> Unit, value: T) {
if (detached) {
return;
return
}
callback(value);
callback(value)
}
}

View File

@@ -0,0 +1,354 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package app.alextran.immich.media
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object AssetMediaPigeonUtils {
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : RuntimeException()
enum class AssetMediaActionStatus(val raw: Int) {
DONE(0),
ALREADY_IN_STATE(1),
NOT_FOUND(2),
FAILED(3);
companion object {
fun ofRaw(raw: Int): AssetMediaActionStatus? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class AssetMediaActionResult (
val id: String,
val status: AssetMediaActionStatus
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): AssetMediaActionResult {
val id = pigeonVar_list[0] as String
val status = pigeonVar_list[1] as AssetMediaActionStatus
return AssetMediaActionResult(id, status)
}
}
fun toList(): List<Any?> {
return listOf(
id,
status,
)
}
override fun equals(other: Any?): Boolean {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
val other = other as AssetMediaActionResult
return AssetMediaPigeonUtils.deepEquals(this.id, other.id) && AssetMediaPigeonUtils.deepEquals(this.status, other.status)
}
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + AssetMediaPigeonUtils.deepHash(this.id)
result = 31 * result + AssetMediaPigeonUtils.deepHash(this.status)
return result
}
}
private open class AssetMediaPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let {
AssetMediaActionStatus.ofRaw(it.toInt())
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AssetMediaActionResult.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is AssetMediaActionStatus -> {
stream.write(129)
writeValue(stream, value.raw.toLong())
}
is AssetMediaActionResult -> {
stream.write(130)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface AssetMediaApi {
fun trash(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit)
fun restore(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit)
fun trashedAmong(ids: List<String>, callback: (Result<List<String>>) -> Unit)
companion object {
/** The codec used by AssetMediaApi. */
val codec: MessageCodec<Any?> by lazy {
AssetMediaPigeonCodec()
}
/** Sets up an instance of `AssetMediaApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: AssetMediaApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trash$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idsArg = args[0] as List<String>
api.trash(idsArg) { result: Result<List<AssetMediaActionResult>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(AssetMediaPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.restore$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idsArg = args[0] as List<String>
api.restore(idsArg) { result: Result<List<AssetMediaActionResult>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(AssetMediaPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trashedAmong$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idsArg = args[0] as List<String>
api.trashedAmong(idsArg) { result: Result<List<String>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(AssetMediaPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(AssetMediaPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,239 @@
package app.alextran.immich.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import app.alextran.immich.core.ImmichPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.UUID
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
private enum class MediaAction { TRASH, RESTORE }
private data class MediaItem(val uri: Uri, val isTrashed: Boolean)
private const val MAX_QUERY_ARGS = 900
private const val MAX_CONSENT_URIS = 500
@SuppressLint("NewApi", "InlinedApi")
class AssetMediaApiImpl(context: Context) : ImmichPlugin(), AssetMediaApi, ActivityAware {
private val ctx: Context = context.applicationContext
private var binaryMessenger: BinaryMessenger? = null
private var activityBinding: ActivityPluginBinding? = null
private val supportsMediaRequest: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
super.onAttachedToEngine(binding)
binaryMessenger = binding.binaryMessenger
AssetMediaApi.setUp(binding.binaryMessenger, this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
super.onDetachedFromEngine(binding)
binaryMessenger?.let { AssetMediaApi.setUp(it, null) }
binaryMessenger = null
activityBinding = null
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
}
override fun onDetachedFromActivityForConfigChanges() {
activityBinding = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activityBinding = binding
}
override fun onDetachedFromActivity() {
activityBinding = null
}
override fun trash(ids: List<String>, callback: (Result<List<AssetMediaActionResult>>) -> Unit) =
runAction(ids, MediaAction.TRASH, callback)
override fun restore(
ids: List<String>,
callback: (Result<List<AssetMediaActionResult>>) -> Unit
) =
runAction(ids, MediaAction.RESTORE, callback)
override fun trashedAmong(ids: List<String>, callback: (Result<List<String>>) -> Unit) =
respond(callback, "TRASHED_AMONG_ERROR") {
if (!supportsMediaRequest) return@respond emptyList()
val items = queryMediaItems(ids)
ids.filter { items[it]?.isTrashed == true }
}
private fun runAction(
ids: List<String>,
action: MediaAction,
callback: (Result<List<AssetMediaActionResult>>) -> Unit,
) = respond(callback, "MEDIA_ACTION_ERROR") {
if (ids.isEmpty()) return@respond emptyList()
if (!supportsMediaRequest) {
return@respond ids.map { AssetMediaActionResult(it, AssetMediaActionStatus.FAILED) }
}
val items = queryMediaItems(ids)
val targets = ids.mapNotNull { id ->
val item = items[id] ?: return@mapNotNull null
val needsAction = when (action) {
MediaAction.TRASH -> !item.isTrashed
MediaAction.RESTORE -> item.isTrashed
}
if (needsAction) id to item.uri else null
}.toMap()
val applied = buildMap {
for (batch in targets.entries.chunked(MAX_CONSENT_URIS)) {
val result = if (requestConsent(action, batch.map { it.value })) {
AssetMediaActionStatus.DONE
} else {
AssetMediaActionStatus.FAILED
}
batch.forEach { put(it.key, result) }
}
}
ids.map { id ->
val status = when (id) {
in applied -> applied.getValue(id)
!in items -> AssetMediaActionStatus.NOT_FOUND
else -> AssetMediaActionStatus.ALREADY_IN_STATE
}
AssetMediaActionResult(id, status)
}
}
private fun <T> respond(callback: (Result<T>) -> Unit, errorCode: String, work: suspend () -> T) {
scope.launch {
try {
completeWhenActive(callback, Result.success(work()))
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
completeWhenActive(callback, Result.failure(FlutterError(errorCode, e.message, null)))
}
}
}
private suspend fun requestConsent(action: MediaAction, uris: List<Uri>): Boolean =
withContext(Dispatchers.Main) {
val activity = activityBinding?.activity as? ComponentActivity ?: return@withContext false
val resolver = ctx.contentResolver
val request = when (action) {
MediaAction.TRASH -> MediaStore.createTrashRequest(resolver, uris, true)
MediaAction.RESTORE -> MediaStore.createTrashRequest(resolver, uris, false)
}
suspendCancellableCoroutine { continuation ->
val key = "immich_asset_media_api_${UUID.randomUUID()}"
var launcher: ActivityResultLauncher<IntentSenderRequest>? = null
launcher = activity.activityResultRegistry.register(
key,
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
launcher?.unregister()
if (continuation.isActive) {
continuation.resume(result.resultCode == Activity.RESULT_OK)
}
}
continuation.invokeOnCancellation { launcher.unregister() }
try {
launcher.launch(IntentSenderRequest.Builder(request.intentSender).build())
} catch (_: Exception) {
launcher.unregister()
if (continuation.isActive) {
continuation.resume(false)
}
}
}
}
private suspend fun queryMediaItems(ids: List<String>): Map<String, MediaItem> =
withContext(Dispatchers.IO) {
val numeric = ids.filter { it.toLongOrNull() != null }
if (numeric.isEmpty()) {
return@withContext emptyMap()
}
buildMap {
for (chunk in numeric.chunked(MAX_QUERY_ARGS)) {
ensureActive()
val placeholders = chunk.joinToString(",") { "?" }
val args = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SELECTION,
"${MediaStore.Files.FileColumns._ID} IN ($placeholders)"
)
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, chunk.toTypedArray())
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
}
ctx.contentResolver.query(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.Files.FileColumns.IS_TRASHED,
),
args,
null,
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val trashedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.IS_TRASHED)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val uri = mediaContentUri(cursor.getInt(typeColumn), id) ?: continue
put(id.toString(), MediaItem(uri, cursor.getInt(trashedColumn) == 1))
}
}
}
}
}
private fun mediaContentUri(mediaType: Int, id: Long): Uri? = when (mediaType) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE ->
ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
)
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO ->
ContentUris.withAppendedId(
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL),
id
)
else -> null
}
}

View File

@@ -1,133 +0,0 @@
package app.alextran.immich.sync
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
class MediaTrashDelegate(
context: Context,
private val trashRequestCode: Int = 1002,
) : PluginRegistry.ActivityResultListener {
private val ctx = context.applicationContext
private var activityBinding: ActivityPluginBinding? = null
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
private fun hasManageMediaPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaStore.canManageMedia(ctx)
} else {
false
}
}
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || !hasManageMediaPermission()) {
callback(Result.failure(FlutterError("PERMISSION_DENIED", "Media permission required", null)))
return
}
val id = mediaId.toLongOrNull()
if (id == null) {
callback(Result.failure(FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)))
return
}
if (!isInTrash(id)) {
callback(Result.failure(FlutterError("TRASH_NOT_FOUND", "Item with id=$id not found in trash", null)))
return
}
restoreUri(ContentUris.withAppendedId(contentUriForType(type.toInt()), id), callback)
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreUri(
contentUri: Uri,
callback: (Result<Boolean>) -> Unit,
) {
val activity = activityBinding?.activity
if (activity == null) {
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
return
}
try {
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, listOf(contentUri), false)
pendingResult = callback
activity.startIntentSenderForResult(
pendingIntent.intentSender,
trashRequestCode,
null,
0,
0,
0,
)
} catch (e: Exception) {
pendingResult = null
callback(
Result.failure(
FlutterError("TRASH_ERROR", "Error creating or starting trash request", e.toString())
)
)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun isInTrash(id: Long): Boolean {
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val args = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
}
return ctx.contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
?.use { it.moveToFirst() } == true
}
private fun contentUriForType(type: Int): Uri =
when (type) {
// Same order as AssetType from Dart.
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}
fun onDetachedFromActivity() {
failPending("ACTIVITY_DETACHED", "Activity detached before trash result")
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == trashRequestCode) {
val callback = pendingResult
pendingResult = null
callback?.invoke(Result.success(resultCode == Activity.RESULT_OK))
return true
}
return false
}
private fun failPending(code: String, message: String) {
val callback = pendingResult ?: return
pendingResult = null
callback(Result.failure(FlutterError(code, message, null)))
}
}

View File

@@ -553,8 +553,6 @@ interface NativeSyncApi {
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
fun cancelHashing()
fun cancelSync()
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
companion object {
@@ -765,42 +763,6 @@ interface NativeSyncApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getTrashedAssets())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val mediaIdArg = args[0] as String
val typeArg = args[1] as Long
api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {

View File

@@ -29,9 +29,4 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
private fun getMediaChanges(): SyncDelta {
throw IllegalStateException("Method not supported on this Android version.")
}
override fun getTrashedAssets(): Map<String, List<PlatformAsset>> {
//Method not supported on this Android version.
return emptyMap()
}
}

View File

@@ -99,29 +99,4 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
// Unmounted volumes are handled in dart when the album is removed
return SyncDelta(hasChanges, changed, deleted, assetAlbums)
}
override fun getTrashedAssets(): Map<String, List<PlatformAsset>> {
val result = LinkedHashMap<String, MutableList<PlatformAsset>>()
val volumes = MediaStore.getExternalVolumeNames(ctx)
for (volume in volumes) {
val queryArgs = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, MEDIA_SELECTION)
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, MEDIA_SELECTION_ARGS)
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}
getCursor(volume, queryArgs).use { cursor ->
getAssets(cursor).forEach { res ->
if (res is AssetResult.ValidAsset) {
result.getOrPut(res.albumId) { mutableListOf() }.add(res.asset)
}
}
}
}
return result.mapValues { it.value.toList() }
}
}

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import androidx.exifinterface.media.ExifInterface
import android.os.Build
import android.os.Bundle
import android.os.ext.SdkExtensions
@@ -17,8 +16,6 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.ImageHeaderParserUtils
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -41,12 +38,11 @@ sealed class AssetResult {
private const val TAG = "NativeSyncApiImplBase"
@SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
private val ctx: Context = context.applicationContext
private var hashTask: Job? = null
private var syncJob: Job? = null
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
companion object {
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
@@ -377,7 +373,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
)?.use { cursor -> cursor.count.toLong() } ?: 0L
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit) {
fun getAssetsForAlbum(
albumId: String,
updatedTimeCond: Long?,
callback: (Result<List<PlatformAsset>>) -> Unit
) {
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
}
@@ -477,33 +477,16 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
try {
completeWhenActive(callback, Result.success(work()))
} catch (e: CancellationException) {
completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)))
completeWhenActive(
callback,
Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null))
)
} catch (e: Exception) {
completeWhenActive(callback, Result.failure(e))
}
}
}
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
mediaTrashDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
mediaTrashDelegate.onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
mediaTrashDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
mediaTrashDelegate.onDetachedFromActivity()
}
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
@Suppress("unused", "UNUSED_PARAMETER")
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {

File diff suppressed because it is too large Load Diff

View File

@@ -26,9 +26,6 @@ Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Local signing overrides.
Signing.local.xcconfig
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
platform :ios, '14.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -45,7 +45,7 @@ post_install do |installer|
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
end
end
end

View File

@@ -12,8 +12,12 @@ PODS:
- Flutter
- fluttertoast (0.0.2):
- Flutter
- home_widget (0.0.1):
- Flutter
- native_video_player (1.0.0):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@@ -30,7 +34,9 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
@@ -47,8 +53,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
native_video_player:
:path: ".symlinks/plugins/native_video_player/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
@@ -61,10 +71,12 @@ SPEC CHECKSUMS:
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
PODFILE CHECKSUM: 3c43a700a4bffb4120bf696cad263aefd4bb3c8c
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
COCOAPODS: 1.16.2

View File

@@ -96,7 +96,6 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
51516E0F2A0000000000C0F1 /* Signing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.xcconfig; sourceTree = "<group>"; };
8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
@@ -154,18 +153,19 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Core;
sourceTree = "<group>";
};
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
B2D5F7B13005FCA00080CE3E /* AssetMedia */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AssetMedia;
sourceTree = "<group>";
};
F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@@ -184,8 +184,6 @@
};
FEE084F22EC172080045228E /* Schemas */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Schemas;
sourceTree = "<group>";
};
@@ -263,7 +261,6 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
51516E0F2A0000000000C0F1 /* Signing.xcconfig */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
FAC6F8B62D287F120078CB2F /* ShareExtension */,
@@ -287,6 +284,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B2D5F7B13005FCA00080CE3E /* AssetMedia */,
FE1BB4562F8319560087DBF9 /* Utility */,
FEE084F22EC172080045228E /* Schemas */,
B231F52D2E93A44A00BC45D1 /* Core */,
@@ -378,6 +376,7 @@
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */,
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -388,6 +387,7 @@
fileSystemSynchronizedGroups = (
B231F52D2E93A44A00BC45D1 /* Core */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
B2D5F7B13005FCA00080CE3E /* AssetMedia */,
FEE084F22EC172080045228E /* Schemas */,
);
name = Runner;
@@ -471,7 +471,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
);
@@ -518,6 +518,27 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -542,10 +563,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -696,7 +721,6 @@
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -727,7 +751,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID).profile";
CUSTOM_GROUP_ID = group.app.immich.share.profile;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -740,7 +764,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = "";
SDKROOT = iphoneos;
@@ -760,16 +784,16 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile;
PRODUCT_NAME = "Immich-Profile";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -780,7 +804,6 @@
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
@@ -811,7 +834,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID).debug";
CUSTOM_GROUP_ID = group.app.immich.share.debug;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -830,7 +853,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES;
@@ -841,7 +864,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 51516E0F2A0000000000C0F1 /* Signing.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
@@ -872,7 +894,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = "$(IMMICH_GROUP_ID)";
CUSTOM_GROUP_ID = group.app.immich.share;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -885,7 +907,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = "";
SDKROOT = iphoneos;
@@ -907,16 +929,16 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug;
PRODUCT_NAME = "Immich-Debug";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -936,16 +958,16 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD)";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = Immich;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -969,7 +991,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -986,7 +1008,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug.Widget";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.Widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
@@ -1012,7 +1034,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -1028,7 +1050,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD).Widget";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.Widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1052,7 +1074,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -1068,7 +1090,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile.Widget";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.Widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1091,7 +1113,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -1108,7 +1130,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).debug.ShareExtension";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.debug.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -1134,7 +1156,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -1150,7 +1172,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_PROD).ShareExtension";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -1174,7 +1196,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = "$(IMMICH_TEAM_ID)";
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
@@ -1190,7 +1212,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(IMMICH_BUNDLE_ID_DEV).profile.ShareExtension";
PRODUCT_BUNDLE_IDENTIFIER = app.futo.immich.profile.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -1246,7 +1268,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};

View File

@@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-sharing",
"state" : {
"revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90",
"version" : "2.9.1"
"revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818",
"version" : "2.7.4"
}
},
{

View File

@@ -26,6 +26,7 @@ import native_video_player
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
AssetMediaApiImpl.register(with: registry.registrar(forPlugin: AssetMediaApiImpl.name)!)
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
@@ -36,5 +37,6 @@ import native_video_player
public static func cancelPlugins(with engine: FlutterEngine) {
(engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine()
(engine.valuePublished(byPlugin: AssetMediaApiImpl.name) as? AssetMediaApiImpl)?.detachFromEngine()
}
}

View File

@@ -0,0 +1,313 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
private func doubleEqualsAssetMedia(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashAssetMedia(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsAssetMedia(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
switch (cleanLhs, cleanRhs) {
case (nil, nil):
return true
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsAssetMedia(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsAssetMedia(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsAssetMedia(lhsKey, rhsKey) {
if deepEqualsAssetMedia(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsAssetMedia(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
return false
}
}
func deepHashAssetMedia(value: Any?, hasher: inout Hasher) {
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashAssetMedia(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashAssetMedia(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashAssetMedia(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashAssetMedia(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashAssetMedia(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
} else {
hasher.combine(0)
}
}
enum AssetMediaActionStatus: Int {
case done = 0
case alreadyInState = 1
case notFound = 2
case failed = 3
}
/// Generated class from Pigeon that represents data sent in messages.
struct AssetMediaActionResult: Hashable {
var id: String
var status: AssetMediaActionStatus
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> AssetMediaActionResult? {
let id = pigeonVar_list[0] as! String
let status = pigeonVar_list[1] as! AssetMediaActionStatus
return AssetMediaActionResult(
id: id,
status: status
)
}
func toList() -> [Any?] {
return [
id,
status,
]
}
static func == (lhs: AssetMediaActionResult, rhs: AssetMediaActionResult) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsAssetMedia(lhs.id, rhs.id) && deepEqualsAssetMedia(lhs.status, rhs.status)
}
func hash(into hasher: inout Hasher) {
hasher.combine("AssetMediaActionResult")
deepHashAssetMedia(value: id, hasher: &hasher)
deepHashAssetMedia(value: status, hasher: &hasher)
}
}
private class AssetMediaPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return AssetMediaActionStatus(rawValue: enumResultAsInt)
}
return nil
case 130:
return AssetMediaActionResult.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
}
}
private class AssetMediaPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? AssetMediaActionStatus {
super.writeByte(129)
super.writeValue(value.rawValue)
} else if let value = value as? AssetMediaActionResult {
super.writeByte(130)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
}
}
private class AssetMediaPigeonCodecReaderWriter: FlutterStandardReaderWriter {
override func reader(with data: Data) -> FlutterStandardReader {
return AssetMediaPigeonCodecReader(data: data)
}
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
return AssetMediaPigeonCodecWriter(data: data)
}
}
class AssetMediaPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = AssetMediaPigeonCodec(readerWriter: AssetMediaPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol AssetMediaApi {
func trash(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void)
func restore(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void)
func trashedAmong(ids: [String], completion: @escaping (Result<[String], Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class AssetMediaApiSetup {
static var codec: FlutterStandardMessageCodec { AssetMediaPigeonCodec.shared }
/// Sets up an instance of `AssetMediaApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AssetMediaApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let trashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
trashChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let idsArg = args[0] as! [String]
api.trash(ids: idsArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
trashChannel.setMessageHandler(nil)
}
let restoreChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.restore\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
restoreChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let idsArg = args[0] as! [String]
api.restore(ids: idsArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
restoreChannel.setMessageHandler(nil)
}
let trashedAmongChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AssetMediaApi.trashedAmong\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
trashedAmongChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let idsArg = args[0] as! [String]
api.trashedAmong(ids: idsArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
trashedAmongChannel.setMessageHandler(nil)
}
}
}

View File

@@ -0,0 +1,26 @@
class AssetMediaApiImpl: ImmichPlugin, AssetMediaApi, FlutterPlugin {
static let name = "AssetMediaApi"
static func register(with registrar: FlutterPluginRegistrar) {
let instance = AssetMediaApiImpl()
AssetMediaApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance)
registrar.publish(instance)
}
func detachFromEngine(for registrar: FlutterPluginRegistrar) {
AssetMediaApiSetup.setUp(binaryMessenger: registrar.messenger(), api: nil)
super.detachFromEngine()
}
func trash(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void) {
completeWhenActive(for: completion, with: .success(ids.map { AssetMediaActionResult(id: $0, status: .failed) }))
}
func restore(ids: [String], completion: @escaping (Result<[AssetMediaActionResult], Error>) -> Void) {
completeWhenActive(for: completion, with: .success(ids.map { AssetMediaActionResult(id: $0, status: .failed) }))
}
func trashedAmong(ids: [String], completion: @escaping (Result<[String], Error>) -> Void) {
completeWhenActive(for: completion, with: .success([]))
}
}

View File

@@ -537,8 +537,6 @@ protocol NativeSyncApi {
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
func cancelHashing() throws
func cancelSync() throws
func getTrashedAssets() throws -> [String: [PlatformAsset]]
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
}
@@ -723,39 +721,6 @@ class NativeSyncApiSetup {
} else {
cancelSyncChannel.setMessageHandler(nil)
}
let getTrashedAssetsChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getTrashedAssetsChannel.setMessageHandler { _, reply in
do {
let result = try api.getTrashedAssets()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getTrashedAssetsChannel.setMessageHandler(nil)
}
let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
restoreFromTrashByIdChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let mediaIdArg = args[0] as! String
let typeArg = args[1] as! Int64
api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
restoreFromTrashByIdChannel.setMessageHandler(nil)
}
let getCloudIdForAssetIdsChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)

View File

@@ -436,14 +436,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
})
}
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
}
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void) {
completion(.success(false))
}
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
// Ensure to actually getting all assets for the Recents album
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {

View File

@@ -1,29 +0,0 @@
// Xcode Signing Constants
//
// These are integrated into the Xcode project at build time to allow for dynamic signing configs
// across our many targets.
//
// Create `Signing.local.xcconfig` overriding each variable to provide your own personal signing
// overrides during local development:
// IMMICH_TEAM_ID = ABCDE12345
// IMMICH_BUNDLE_ID_PROD = com.customuniqueid.immich
// IMMICH_BUNDLE_ID_DEV = com.customuniqueid.immichdev
// IMMICH_GROUP_ID = group.com.customuniqueid.immich
// Apple Developer Team ID used for automatically provisioning signing credentials
IMMICH_TEAM_ID = 2W7AC6T8T5
// Root bundle identifier for the Release configuration.
// Extension suffixes (`.ShareExtension`/`.Widget`) will be appended
IMMICH_BUNDLE_ID_PROD = app.alextran.immich
// Root bundle identifier for the Debug and Profile configurations.
// Build scheme suffixes (`.debug`/`.profile`) will be appended
IMMICH_BUNDLE_ID_DEV = app.futo.immich
// Root App Group identifier.
// Build scheme suffixes (`.debug`/`.profile`) will be appended
IMMICH_GROUP_ID = group.app.immich.share
// Optional per-developer configuration override
#include? "Signing.local.xcconfig"

View File

@@ -26,3 +26,17 @@ enum SlideshowLook { contain, cover, blurredBackground }
enum SlideshowDirection { forward, backward, shuffle }
enum PartnerDirection { sharedBy, sharedWith }
enum TrashSyncStatus {
pending,
trashed,
/* The asset was restored outside of Immich. The implications of this are:
* - Assets matching the same checksum won't be re-trashed
* - Backups will ignore the asset from re-uploading
*/
dismissed,
/* The asset was restored back out of the OS trash by us. This allows us
* to copy the checksum back into the local asset table
*/
restored,
}

View File

@@ -70,10 +70,6 @@ class RemoteAsset extends BaseAsset {
bool get isTrashed => deletedAt != null;
bool get isStacked => stackId != null;
bool get isArchived => visibility == .archive;
@override
String toString() {
return '''Asset {

View File

@@ -23,6 +23,7 @@ const defaultConfig = AppConfig();
class AppConfig {
final LogLevel logLevel;
final bool trashSyncEnabled;
final ThemeConfig theme;
final CleanupConfig cleanup;
final MapConfig map;
@@ -38,6 +39,7 @@ class AppConfig {
const AppConfig({
this.logLevel = .info,
this.trashSyncEnabled = false,
this.theme = const .new(),
this.cleanup = const .new(),
this.map = const .new(),
@@ -54,6 +56,7 @@ class AppConfig {
AppConfig copyWith({
LogLevel? logLevel,
bool? trashSyncEnabled,
ThemeConfig? theme,
CleanupConfig? cleanup,
MapConfig? map,
@@ -68,6 +71,7 @@ class AppConfig {
FeatureMessageConfig? featureMessage,
}) => .new(
logLevel: logLevel ?? this.logLevel,
trashSyncEnabled: trashSyncEnabled ?? this.trashSyncEnabled,
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
map: map ?? this.map,
@@ -87,6 +91,7 @@ class AppConfig {
identical(this, other) ||
(other is AppConfig &&
other.logLevel == logLevel &&
other.trashSyncEnabled == trashSyncEnabled &&
other.theme == theme &&
other.cleanup == cleanup &&
other.map == map &&
@@ -103,6 +108,7 @@ class AppConfig {
@override
int get hashCode => Object.hash(
logLevel,
trashSyncEnabled,
theme,
cleanup,
map,
@@ -119,17 +125,17 @@ class AppConfig {
@override
String toString() =>
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share, featureMessage: $featureMessage)';
'AppConfig(logLevel: $logLevel, trashSyncEnabled: $trashSyncEnabled, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share, featureMessage: $featureMessage)';
T read<T>(SettingsKey<T> key) =>
(switch (key) {
.logLevel => logLevel,
.trashSyncEnabled => trashSyncEnabled,
.themePrimaryColor => theme.primaryColor,
.themeMode => theme.mode,
.themeDynamic => theme.dynamicTheme,
.themeColorfulInterface => theme.colorfulInterface,
.imagePreferRemote => image.preferRemote,
.imageLoadPreview => image.loadPreview,
.imageLoadOriginal => image.loadOriginal,
.viewerLoopVideo => viewer.loopVideo,
.viewerLoadOriginalVideo => viewer.loadOriginalVideo,
@@ -177,12 +183,12 @@ class AppConfig {
AppConfig write<T, U extends T>(SettingsKey<T> key, U value) {
return switch (key) {
.logLevel => copyWith(logLevel: value as LogLevel),
.trashSyncEnabled => copyWith(trashSyncEnabled: value as bool),
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
.themeMode => copyWith(theme: theme.copyWith(mode: value as ThemeMode)),
.themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)),
.themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)),
.imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)),
.imageLoadPreview => copyWith(image: image.copyWith(loadPreview: value as bool)),
.imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)),
.viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)),
.viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)),

View File

@@ -1,28 +1,20 @@
class ImageConfig {
final bool preferRemote;
final bool loadPreview;
final bool loadOriginal;
const ImageConfig({this.preferRemote = false, this.loadPreview = true, this.loadOriginal = false});
const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
ImageConfig copyWith({bool? preferRemote, bool? loadPreview, bool? loadOriginal}) => ImageConfig(
preferRemote: preferRemote ?? this.preferRemote,
loadPreview: loadPreview ?? this.loadPreview,
loadOriginal: loadOriginal ?? this.loadOriginal,
);
ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ImageConfig &&
other.preferRemote == preferRemote &&
other.loadPreview == loadPreview &&
other.loadOriginal == loadOriginal);
(other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
@override
int get hashCode => Object.hash(preferRemote, loadPreview, loadOriginal);
int get hashCode => Object.hash(preferRemote, loadOriginal);
@override
String toString() =>
'ImageConfig(preferRemoteImage: $preferRemote, loadPreview: $loadPreview, loadOriginal: $loadOriginal)';
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
}

View File

@@ -17,7 +17,6 @@ enum SettingsKey<T> {
// Image
imagePreferRemote<bool>(),
imageLoadPreview<bool>(),
imageLoadOriginal<bool>(),
// Viewer
@@ -38,6 +37,9 @@ enum SettingsKey<T> {
albumIsReverse<bool>(),
albumIsGrid<bool>(),
// Sync
trashSyncEnabled<bool>(),
// Backup
backupEnabled<bool>(),
backupUseCellularForVideos<bool>(),

View File

@@ -12,13 +12,13 @@ enum StoreKey<T> {
advancedTroubleshooting<bool>._(114),
enableHapticFeedback<bool>._(126),
manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings
readonlyModeEnabled<bool>._(138),
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store
legacyManageLocalMediaAndroid<bool>._(137),
legacyBackupRequireCharging<bool>._(7),
legacyBackupTriggerDelay<int>._(8),
legacySyncAlbums<bool>._(131),

View File

@@ -4,10 +4,8 @@ import 'package:flutter/services.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.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/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:logging/logging.dart';
@@ -17,7 +15,6 @@ class HashService {
final int _batchSize;
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final NativeSyncApi _nativeSyncApi;
final Completer<void>? _cancellation;
final _log = Logger('HashService');
@@ -25,7 +22,6 @@ class HashService {
HashService({
required this._localAlbumRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._nativeSyncApi,
this._cancellation,
int? batchSize,
@@ -58,14 +54,6 @@ class HashService {
await _hashAssets(album, assetsToHash);
}
}
if (CurrentPlatform.isAndroid && localAlbums.isNotEmpty) {
final backupAlbumIds = localAlbums.map((e) => e.id);
final trashedToHash = await _trashedLocalAssetRepository.getAssetsToHash(backupAlbumIds);
if (trashedToHash.isNotEmpty) {
final pseudoAlbum = LocalAlbum(id: '-pseudoAlbum', name: 'Trash', updatedAt: DateTime.now());
await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true);
}
}
} on PlatformException catch (e) {
if (e.code == _kHashCancelledCode) {
_log.warning("Hashing cancelled by platform");
@@ -82,7 +70,7 @@ class HashService {
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
/// with hash for those that were successfully hashed. Hashes are looked up in a table
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash, {bool isTrashed = false}) async {
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
final toHash = <String, LocalAsset>{};
for (final asset in assetsToHash) {
@@ -93,16 +81,16 @@ class HashService {
toHash[asset.id] = asset;
if (toHash.length == _batchSize) {
await _processBatch(album, toHash, isTrashed);
await _processBatch(album, toHash);
toHash.clear();
}
}
await _processBatch(album, toHash, isTrashed);
await _processBatch(album, toHash);
}
/// Processes a batch of assets.
Future<void> _processBatch(LocalAlbum album, Map<String, LocalAsset> toHash, bool isTrashed) async {
Future<void> _processBatch(LocalAlbum album, Map<String, LocalAsset> toHash) async {
if (toHash.isEmpty) {
return;
}
@@ -137,10 +125,6 @@ class HashService {
}
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
if (isTrashed) {
await _trashedLocalAssetRepository.updateHashes(hashed);
} else {
await _localAssetRepository.updateHashes(hashed);
}
await _localAssetRepository.updateHashes(hashed);
}
}

View File

@@ -5,15 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.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/trashed_local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
@@ -24,20 +20,16 @@ class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository;
// ignore: unused_field
final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashSyncRepository _trashSyncRepository;
final NativeSyncApi _nativeSyncApi;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final AssetMediaRepository _assetMediaRepository;
final IPermissionRepository _permissionRepository;
final Completer<void>? _cancellation;
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required this._localAlbumRepository,
required this._localAssetRepository,
required this._trashSyncRepository,
required this._nativeSyncApi,
required this._trashedLocalAssetRepository,
required this._assetMediaRepository,
required this._permissionRepository,
this._cancellation,
}) {
_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning));
@@ -48,15 +40,6 @@ class LocalSyncService {
Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start();
try {
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
final hasPermission = await _permissionRepository.hasManageMediaPermission();
if (hasPermission) {
await _syncTrashedAssets();
} else {
_log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing");
}
}
if (CurrentPlatform.isIOS) {
// final assets = await _localAssetRepository.getEmptyCloudIdAssets();
// await _mapIosCloudIds(assets);
@@ -119,6 +102,7 @@ class LocalSyncService {
await _mapIosCloudIds(newAssets);
}
await _nativeSyncApi.checkpointSync();
await _trashSyncRepository.restoreChecksums();
} on PlatformException catch (e, s) {
if (e.code == _kSyncCancelledCode) {
_log.warning("Local sync cancelled");
@@ -150,6 +134,7 @@ class LocalSyncService {
);
await _nativeSyncApi.checkpointSync();
await _trashSyncRepository.restoreChecksums();
stopwatch.stop();
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
} on PlatformException catch (e, s) {
@@ -383,48 +368,6 @@ class LocalSyncService {
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
}
Future<void> _syncTrashedAssets() async {
final trashedAssetMap = await _nativeSyncApi.getTrashedAssets();
await processTrashedAssets(trashedAssetMap);
}
@visibleForTesting
Future<void> processTrashedAssets(Map<String, List<PlatformAsset>> trashedAssetMap) async {
if (trashedAssetMap.isEmpty) {
_log.info("syncTrashedAssets, No trashed assets found");
}
final trashedAssets = trashedAssetMap.cast<String, List<Object?>>().entries.expand(
(entry) => entry.value.cast<PlatformAsset>().toTrashedAssets(entry.key),
);
_log.fine("syncTrashedAssets, trashedAssets: ${trashedAssets.map((e) => e.asset.id)}");
await _trashedLocalAssetRepository.processTrashSnapshot(trashedAssets);
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) {
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
} else {
_log.info("syncTrashedAssets, No remote assets found for restoration");
}
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
if (localAssetsToTrash.isNotEmpty) {
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
_log.info("Moving to trash ${localIds.join(", ")} assets");
final movedIds = await _assetMediaRepository.deleteAll(localIds);
if (movedIds.isNotEmpty) {
final movedAssetsByAlbum = localAssetsToTrash.map(
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
)..removeWhere((_, assets) => assets.isEmpty);
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
}
} else {
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
}
}
}
extension on Iterable<PlatformAlbum> {
@@ -445,10 +388,6 @@ extension on Iterable<PlatformAsset> {
List<LocalAsset> toLocalAssets() {
return map((e) => e.toLocalAsset()).toList();
}
Iterable<TrashedAsset> toTrashedAssets(String albumId) {
return map((e) => (albumId: albumId, asset: e.toLocalAsset()));
}
}
extension PlatformToLocalAsset on PlatformAsset {

View File

@@ -3,18 +3,13 @@
import 'dart:async';
import 'dart:convert';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:logging/logging.dart';
@@ -33,10 +28,7 @@ class SyncStreamService {
final SyncApiRepository _syncApiRepository;
final SyncStreamRepository _syncStreamRepository;
final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final AssetMediaRepository _assetMediaRepository;
final IPermissionRepository _permissionRepository;
final DriftTrashSyncRepository _trashSyncRepository;
final SyncMigrationRepository _syncMigrationRepository;
final ApiService _api;
final Completer<void>? _cancellation;
@@ -44,10 +36,7 @@ class SyncStreamService {
SyncStreamService({
required this._syncApiRepository,
required this._syncStreamRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._assetMediaRepository,
required this._permissionRepository,
required this._trashSyncRepository,
required this._syncMigrationRepository,
required this._api,
this._cancellation,
@@ -202,24 +191,13 @@ class SyncStreamService {
case SyncEntityType.partnerDeleteV1:
return _syncStreamRepository.deletePartnerV1(data.cast());
case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
}
return;
return _syncStreamRepository.updateAssetsV1(data.cast<SyncAssetV1>());
case SyncEntityType.assetV2:
final remoteSyncAssets = data.cast<SyncAssetV2>();
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
}
return;
return _syncStreamRepository.updateAssetsV2(data.cast<SyncAssetV2>());
case SyncEntityType.assetDeleteV1:
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
}
// Has to be before the delete call so we can record the checksums of the assets as well
await _trashSyncRepository.recordHardDeletedChecksums(remoteSyncAssets.map((e) => e.assetId));
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast());
@@ -499,59 +477,4 @@ class SyncStreamService {
_logger.severe("Error processing AssetEditReadyV2 websocket event", error, stackTrace);
}
}
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return Future.value();
} else {
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds);
if (localAssetsToTrash.isNotEmpty) {
await _trashLocalAssets(localAssetsToTrash);
} else {
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
}
}
}
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
_logger.info("Moving to trash ${localIds.join(", ")} assets");
final movedIds = await _assetMediaRepository.deleteAll(localIds);
if (movedIds.isNotEmpty) {
final movedAssetsByAlbum = localAssetsToTrash.map(
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
)..removeWhere((_, assets) => assets.isEmpty);
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
}
}
Future<void> _applyRemoteRestoreToLocal() async {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) {
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
} else {
_logger.info("No remote assets found for restoration");
}
}
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
if (!(await _permissionRepository.hasManageMediaPermission())) {
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await _handleRemoteDeleted(remoteIds);
await _applyRemoteRestoreToLocal();
}
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
if (!(await _permissionRepository.hasManageMediaPermission())) {
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await _handleRemoteDeleted(remoteIds);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/platform/asset_media_api.g.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:logging/logging.dart';
class TrashSyncService {
final DriftTrashSyncRepository _repo;
final AssetMediaApi _assetMediaApi;
final PermissionRepository _permission;
final SettingsRepository _settings;
final Logger _log = Logger('TrashSyncService');
TrashSyncService({
required this._repo,
required this._assetMediaApi,
required this._permission,
required this._settings,
});
Future<void> reconcile() async {
try {
await _prune();
if (!_settings.appConfig.trashSyncEnabled || !await _canApplyToOsTrash()) {
return;
}
await _record();
await _act();
} catch (error, stack) {
_log.severe("Trash reconcile failed", error, stack);
}
}
Future<void> _prune() async {
await _repo.pruneStaleMarkers();
await _repo.pruneDismissedMarkers();
await _repo.prunePendingMarkers();
}
Future<void> _record() async {
await _repo.recordSoftDeleteAssets();
await _repo.recordHardDeletedAssets();
}
Future<void> _act() async {
await _trashAssets();
await _restoreAssets();
await _reconcileWithOSTrash();
}
Future<void> _trashAssets() async {
final pending = await _repo.getPendingAssetIds();
if (pending.isEmpty) {
return;
}
final results = await _assetMediaApi.trash(pending);
final movedIds = results.whereStatusIn(const {.done, .alreadyInState});
final removedIds = results.whereStatusIn(const {.notFound});
if (movedIds.isNotEmpty) {
await _repo.markTrashed(movedIds);
}
if (removedIds.isNotEmpty) {
await _repo.deleteMarkers(removedIds);
}
_log.fine("Trashed ${movedIds.length}, dropped ${removedIds.length} out of ${pending.length} pending");
}
Future<void> _restoreAssets() async {
final restorable = await _repo.getRestorableAssetIds();
if (restorable.isEmpty) {
return;
}
final results = await _assetMediaApi.restore(restorable);
final restoredIds = results.whereStatusIn(const {.done, .alreadyInState});
final goneIds = results.whereStatusIn(const {.notFound});
if (restoredIds.isNotEmpty) {
await _repo.markRestored(restoredIds);
}
if (goneIds.isNotEmpty) {
await _repo.deleteMarkers(goneIds);
}
}
Future<void> _reconcileWithOSTrash() async {
final trashed = await _repo.getTrashedAssetIds();
if (trashed.isEmpty) {
return;
}
final stillTrashed = (await _assetMediaApi.trashedAmong(trashed)).toSet();
final gone = trashed.where((id) => !stillTrashed.contains(id)).toList();
if (gone.isNotEmpty) {
await _repo.reconcileTrashed(gone);
}
}
Future<bool> _canApplyToOsTrash() async {
if (!CurrentPlatform.isAndroid) {
return false;
}
final hasPermission = await _permission.hasManageMediaPermission();
if (!hasPermission) {
_log.fine("OS trash sync skipped: MANAGE_MEDIA permission not granted");
}
return hasPermission;
}
}
extension on Iterable<AssetMediaActionResult> {
Set<String> whereStatusIn(Set<AssetMediaActionStatus> statuses) =>
where((r) => statuses.contains(r.status)).map((r) => r.id).toSet();
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart' as m;
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -33,6 +34,7 @@ class BackgroundSyncManager {
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _linkedAlbumSyncTask;
Cancelable<void>? _hashTask;
Cancelable<void>? _trashSyncTask;
BackgroundSyncManager({
this.onRemoteSyncStart,
@@ -57,6 +59,7 @@ class BackgroundSyncManager {
_linkedAlbumSyncTask,
_deviceAlbumSyncTask,
_hashTask,
_trashSyncTask,
];
final futures = [
for (final task in tasks)
@@ -71,6 +74,7 @@ class BackgroundSyncManager {
_linkedAlbumSyncTask = null;
_deviceAlbumSyncTask = null;
_hashTask = null;
_trashSyncTask = null;
try {
await Future.wait(futures);
@@ -160,6 +164,21 @@ class BackgroundSyncManager {
});
}
Future<void> syncTrash() {
if (_trashSyncTask != null) {
return _trashSyncTask!.future;
}
_trashSyncTask = runInIsolateGentle(
computation: (ref) => ref.read(trashSyncServiceProvider).reconcile(),
debugLabel: 'trash-sync',
);
return _trashSyncTask!.whenComplete(() {
_trashSyncTask = null;
});
}
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;

View File

@@ -44,7 +44,16 @@ final class $$AssetEditEntityTableReferences
static i5.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias('asset_edit_entity__asset_id__remote_asset_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$AssetEditEntityTable>('asset_edit_entity')
.assetId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i5.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;

View File

@@ -59,7 +59,16 @@ final class $$AssetFaceEntityTableReferences
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias('asset_face_entity__asset_id__remote_asset_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
.assetId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
@@ -82,7 +91,16 @@ final class $$AssetFaceEntityTableReferences
static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i6.$PersonEntityTable>('person_entity')
.createAlias('asset_face_entity__person_id__person_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
.personId,
i5.ReadDatabaseContainer(
db,
).resultSet<i6.$PersonEntityTable>('person_entity').id,
),
);
i6.$$PersonEntityTableProcessedTableManager? get personId {
final $_column = $_itemColumn<String>('person_id');

View File

@@ -61,7 +61,16 @@ final class $$AssetOcrEntityTableReferences
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias('asset_ocr_entity__asset_id__remote_asset_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(
db,
).resultSet<i1.$AssetOcrEntityTable>('asset_ocr_entity').assetId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;

View File

@@ -75,7 +75,16 @@ final class $$RemoteExifEntityTableReferences
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias('remote_exif_entity__asset_id__remote_asset_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteExifEntityTable>('remote_exif_entity')
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;

View File

@@ -50,7 +50,14 @@ final class $$LocalAlbumEntityTableReferences
) => i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
.createAlias(
'local_album_entity__linked_remote_album_id__remote_album_entity__id',
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumEntityTable>('local_album_entity')
.linkedRemoteAlbumId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity').id,
),
);
i5.$$RemoteAlbumEntityTableProcessedTableManager? get linkedRemoteAlbumId {

View File

@@ -41,7 +41,16 @@ final class $$LocalAlbumAssetEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity')
.createAlias(
'local_album_asset_entity__asset_id__local_asset_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumAssetEntityTable>(
'local_album_asset_entity',
)
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$LocalAssetEntityTable>('local_asset_entity').id,
),
);
i3.$$LocalAssetEntityTableProcessedTableManager get assetId {
@@ -66,7 +75,16 @@ final class $$LocalAlbumAssetEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity')
.createAlias(
'local_album_asset_entity__album_id__local_album_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumAssetEntityTable>(
'local_album_asset_entity',
)
.albumId,
i4.ReadDatabaseContainer(
db,
).resultSet<i5.$LocalAlbumEntityTable>('local_album_entity').id,
),
);
i5.$$LocalAlbumEntityTableProcessedTableManager get albumId {

View File

@@ -53,7 +53,16 @@ final class $$MemoryEntityTableReferences
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias('memory_entity__owner_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(
db,
).resultSet<i1.$MemoryEntityTable>('memory_entity').ownerId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$UserEntityTable>('user_entity').id,
),
);
i5.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;

View File

@@ -39,7 +39,14 @@ final class $$MemoryAssetEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
'memory_asset_entity__asset_id__remote_asset_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
@@ -63,7 +70,16 @@ final class $$MemoryAssetEntityTableReferences
static i5.$MemoryEntityTable _memoryIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i5.$MemoryEntityTable>('memory_entity')
.createAlias('memory_asset_entity__memory_id__memory_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
.memoryId,
i4.ReadDatabaseContainer(
db,
).resultSet<i5.$MemoryEntityTable>('memory_entity').id,
),
);
i5.$$MemoryEntityTableProcessedTableManager get memoryId {
final $_column = $_itemColumn<String>('memory_id')!;

View File

@@ -39,7 +39,16 @@ final class $$PartnerEntityTableReferences
static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias('partner_entity__shared_by_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(
db,
).resultSet<i1.$PartnerEntityTable>('partner_entity').sharedById,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$UserEntityTable>('user_entity').id,
),
);
i4.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<String>('shared_by_id')!;
@@ -62,7 +71,16 @@ final class $$PartnerEntityTableReferences
static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias('partner_entity__shared_with_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedWithId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$UserEntityTable>('user_entity').id,
),
);
i4.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<String>('shared_with_id')!;

View File

@@ -48,7 +48,16 @@ final class $$PersonEntityTableReferences
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias('person_entity__owner_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(
db,
).resultSet<i1.$PersonEntityTable>('person_entity').ownerId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$UserEntityTable>('user_entity').id,
),
);
i4.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;

View File

@@ -52,7 +52,14 @@ final class $$RemoteAlbumEntityTableReferences
) => i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
'remote_album_entity__thumbnail_asset_id__remote_asset_entity__id',
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumEntityTable>('remote_album_entity')
.thumbnailAssetId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i5.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {

View File

@@ -39,7 +39,16 @@ final class $$RemoteAlbumAssetEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
'remote_album_asset_entity__asset_id__remote_asset_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumAssetEntityTable>(
'remote_album_asset_entity',
)
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
@@ -64,7 +73,16 @@ final class $$RemoteAlbumAssetEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
.createAlias(
'remote_album_asset_entity__album_id__remote_album_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumAssetEntityTable>(
'remote_album_asset_entity',
)
.albumId,
i4.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity').id,
),
);
i5.$$RemoteAlbumEntityTableProcessedTableManager get albumId {

View File

@@ -42,7 +42,16 @@ final class $$RemoteAlbumUserEntityTableReferences
i5.ReadDatabaseContainer(db)
.resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity')
.createAlias(
'remote_album_user_entity__album_id__remote_album_entity__id',
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumUserEntityTable>(
'remote_album_user_entity',
)
.albumId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$RemoteAlbumEntityTable>('remote_album_entity').id,
),
);
i4.$$RemoteAlbumEntityTableProcessedTableManager get albumId {
@@ -66,7 +75,18 @@ final class $$RemoteAlbumUserEntityTableReferences
static i6.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i6.$UserEntityTable>('user_entity')
.createAlias('remote_album_user_entity__user_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAlbumUserEntityTable>(
'remote_album_user_entity',
)
.userId,
i5.ReadDatabaseContainer(
db,
).resultSet<i6.$UserEntityTable>('user_entity').id,
),
);
i6.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;

View File

@@ -16,6 +16,11 @@ ON remote_asset_entity (owner_id, library_id, checksum)
WHERE (library_id IS NOT NULL);
''')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_asset_soft_deleted_checksum
ON remote_asset_entity (checksum)
WHERE deleted_at IS NOT NULL
''')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created

View File

@@ -74,7 +74,16 @@ final class $$RemoteAssetEntityTableReferences
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias('remote_asset_entity__owner_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAssetEntityTable>('remote_asset_entity')
.ownerId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$UserEntityTable>('user_entity').id,
),
);
i5.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;
@@ -1762,6 +1771,10 @@ i0.Index get idxRemoteAssetChecksum => i0.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
i0.Index get idxRemoteAssetSoftDeletedChecksum => i0.Index(
'idx_remote_asset_soft_deleted_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_soft_deleted_checksum ON remote_asset_entity (checksum) WHERE deleted_at IS NOT NULL',
);
i0.Index get idxRemoteAssetStackId => i0.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',

View File

@@ -45,7 +45,16 @@ final class $$RemoteAssetCloudIdEntityTableReferences
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
'remote_asset_cloud_id_entity__asset_id__remote_asset_entity__id',
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAssetCloudIdEntityTable>(
'remote_asset_cloud_id_entity',
)
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {

View File

@@ -0,0 +1,14 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class ServerDeletedChecksumEntity extends Table with DriftDefaultsMixin {
const ServerDeletedChecksumEntity();
TextColumn get checksum => text()();
@override
Set<Column> get primaryKey => {checksum};
@override
String get tableName => "server_deleted_checksum";
}

View File

@@ -0,0 +1,318 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/server_deleted_checksum.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/server_deleted_checksum.entity.dart'
as i2;
typedef $$ServerDeletedChecksumEntityTableCreateCompanionBuilder =
i1.ServerDeletedChecksumEntityCompanion Function({
required String checksum,
});
typedef $$ServerDeletedChecksumEntityTableUpdateCompanionBuilder =
i1.ServerDeletedChecksumEntityCompanion Function({
i0.Value<String> checksum,
});
class $$ServerDeletedChecksumEntityTableFilterComposer
extends
i0.Composer<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable
> {
$$ServerDeletedChecksumEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$ServerDeletedChecksumEntityTableOrderingComposer
extends
i0.Composer<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable
> {
$$ServerDeletedChecksumEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$ServerDeletedChecksumEntityTableAnnotationComposer
extends
i0.Composer<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable
> {
$$ServerDeletedChecksumEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
}
class $$ServerDeletedChecksumEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable,
i1.ServerDeletedChecksumEntityData,
i1.$$ServerDeletedChecksumEntityTableFilterComposer,
i1.$$ServerDeletedChecksumEntityTableOrderingComposer,
i1.$$ServerDeletedChecksumEntityTableAnnotationComposer,
$$ServerDeletedChecksumEntityTableCreateCompanionBuilder,
$$ServerDeletedChecksumEntityTableUpdateCompanionBuilder,
(
i1.ServerDeletedChecksumEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable,
i1.ServerDeletedChecksumEntityData
>,
),
i1.ServerDeletedChecksumEntityData,
i0.PrefetchHooks Function()
> {
$$ServerDeletedChecksumEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$ServerDeletedChecksumEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$ServerDeletedChecksumEntityTableFilterComposer(
$db: db,
$table: table,
),
createOrderingComposer: () =>
i1.$$ServerDeletedChecksumEntityTableOrderingComposer(
$db: db,
$table: table,
),
createComputedFieldComposer: () =>
i1.$$ServerDeletedChecksumEntityTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({i0.Value<String> checksum = const i0.Value.absent()}) =>
i1.ServerDeletedChecksumEntityCompanion(checksum: checksum),
createCompanionCallback: ({required String checksum}) =>
i1.ServerDeletedChecksumEntityCompanion.insert(
checksum: checksum,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$ServerDeletedChecksumEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable,
i1.ServerDeletedChecksumEntityData,
i1.$$ServerDeletedChecksumEntityTableFilterComposer,
i1.$$ServerDeletedChecksumEntityTableOrderingComposer,
i1.$$ServerDeletedChecksumEntityTableAnnotationComposer,
$$ServerDeletedChecksumEntityTableCreateCompanionBuilder,
$$ServerDeletedChecksumEntityTableUpdateCompanionBuilder,
(
i1.ServerDeletedChecksumEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$ServerDeletedChecksumEntityTable,
i1.ServerDeletedChecksumEntityData
>,
),
i1.ServerDeletedChecksumEntityData,
i0.PrefetchHooks Function()
>;
class $ServerDeletedChecksumEntityTable extends i2.ServerDeletedChecksumEntity
with
i0.TableInfo<
$ServerDeletedChecksumEntityTable,
i1.ServerDeletedChecksumEntityData
> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$ServerDeletedChecksumEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
'checksum',
);
@override
late final i0.GeneratedColumn<String> checksum = i0.GeneratedColumn<String>(
'checksum',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
@override
List<i0.GeneratedColumn> get $columns => [checksum];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'server_deleted_checksum';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.ServerDeletedChecksumEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('checksum')) {
context.handle(
_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta),
);
} else if (isInserting) {
context.missing(_checksumMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {checksum};
@override
i1.ServerDeletedChecksumEntityData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.ServerDeletedChecksumEntityData(
checksum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}checksum'],
)!,
);
}
@override
$ServerDeletedChecksumEntityTable createAlias(String alias) {
return $ServerDeletedChecksumEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class ServerDeletedChecksumEntityData extends i0.DataClass
implements i0.Insertable<i1.ServerDeletedChecksumEntityData> {
final String checksum;
const ServerDeletedChecksumEntityData({required this.checksum});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['checksum'] = i0.Variable<String>(checksum);
return map;
}
factory ServerDeletedChecksumEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return ServerDeletedChecksumEntityData(
checksum: serializer.fromJson<String>(json['checksum']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{'checksum': serializer.toJson<String>(checksum)};
}
i1.ServerDeletedChecksumEntityData copyWith({String? checksum}) =>
i1.ServerDeletedChecksumEntityData(checksum: checksum ?? this.checksum);
ServerDeletedChecksumEntityData copyWithCompanion(
i1.ServerDeletedChecksumEntityCompanion data,
) {
return ServerDeletedChecksumEntityData(
checksum: data.checksum.present ? data.checksum.value : this.checksum,
);
}
@override
String toString() {
return (StringBuffer('ServerDeletedChecksumEntityData(')
..write('checksum: $checksum')
..write(')'))
.toString();
}
@override
int get hashCode => checksum.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.ServerDeletedChecksumEntityData &&
other.checksum == this.checksum);
}
class ServerDeletedChecksumEntityCompanion
extends i0.UpdateCompanion<i1.ServerDeletedChecksumEntityData> {
final i0.Value<String> checksum;
const ServerDeletedChecksumEntityCompanion({
this.checksum = const i0.Value.absent(),
});
ServerDeletedChecksumEntityCompanion.insert({required String checksum})
: checksum = i0.Value(checksum);
static i0.Insertable<i1.ServerDeletedChecksumEntityData> custom({
i0.Expression<String>? checksum,
}) {
return i0.RawValuesInsertable({if (checksum != null) 'checksum': checksum});
}
i1.ServerDeletedChecksumEntityCompanion copyWith({
i0.Value<String>? checksum,
}) {
return i1.ServerDeletedChecksumEntityCompanion(
checksum: checksum ?? this.checksum,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ServerDeletedChecksumEntityCompanion(')
..write('checksum: $checksum')
..write(')'))
.toString();
}
}

View File

@@ -38,7 +38,16 @@ final class $$StackEntityTableReferences
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias('stack_entity__owner_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i5.ReadDatabaseContainer(
db,
).resultSet<i1.$StackEntityTable>('stack_entity').ownerId,
i5.ReadDatabaseContainer(
db,
).resultSet<i4.$UserEntityTable>('user_entity').id,
),
);
i4.$$UserEntityTableProcessedTableManager get ownerId {
final $_column = $_itemColumn<String>('owner_id')!;

View File

@@ -0,0 +1,22 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync (checksum)')
class TrashSyncEntity extends Table with DriftDefaultsMixin {
const TrashSyncEntity();
TextColumn get assetId => text()();
TextColumn get checksum => text()();
IntColumn get status => intEnum<TrashSyncStatus>().withDefault(Constant(TrashSyncStatus.pending.index))();
DateTimeColumn get assetUpdatedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {assetId};
@override
String get tableName => "trash_sync";
}

View File

@@ -0,0 +1,524 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart'
as i1;
import 'package:immich_mobile/constants/enums.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$TrashSyncEntityTableCreateCompanionBuilder =
i1.TrashSyncEntityCompanion Function({
required String assetId,
required String checksum,
i0.Value<i2.TrashSyncStatus> status,
i0.Value<DateTime?> assetUpdatedAt,
});
typedef $$TrashSyncEntityTableUpdateCompanionBuilder =
i1.TrashSyncEntityCompanion Function({
i0.Value<String> assetId,
i0.Value<String> checksum,
i0.Value<i2.TrashSyncStatus> status,
i0.Value<DateTime?> assetUpdatedAt,
});
class $$TrashSyncEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get assetId => $composableBuilder(
column: $table.assetId,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<i2.TrashSyncStatus, i2.TrashSyncStatus, int>
get status => $composableBuilder(
column: $table.status,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<DateTime> get assetUpdatedAt => $composableBuilder(
column: $table.assetUpdatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$TrashSyncEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get assetId => $composableBuilder(
column: $table.assetId,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get status => $composableBuilder(
column: $table.status,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get assetUpdatedAt => $composableBuilder(
column: $table.assetUpdatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$TrashSyncEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get assetId =>
$composableBuilder(column: $table.assetId, builder: (column) => column);
i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.TrashSyncStatus, int> get status =>
$composableBuilder(column: $table.status, builder: (column) => column);
i0.GeneratedColumn<DateTime> get assetUpdatedAt => $composableBuilder(
column: $table.assetUpdatedAt,
builder: (column) => column,
);
}
class $$TrashSyncEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData,
i1.$$TrashSyncEntityTableFilterComposer,
i1.$$TrashSyncEntityTableOrderingComposer,
i1.$$TrashSyncEntityTableAnnotationComposer,
$$TrashSyncEntityTableCreateCompanionBuilder,
$$TrashSyncEntityTableUpdateCompanionBuilder,
(
i1.TrashSyncEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData
>,
),
i1.TrashSyncEntityData,
i0.PrefetchHooks Function()
> {
$$TrashSyncEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$TrashSyncEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$TrashSyncEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$TrashSyncEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$TrashSyncEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<String> checksum = const i0.Value.absent(),
i0.Value<i2.TrashSyncStatus> status = const i0.Value.absent(),
i0.Value<DateTime?> assetUpdatedAt = const i0.Value.absent(),
}) => i1.TrashSyncEntityCompanion(
assetId: assetId,
checksum: checksum,
status: status,
assetUpdatedAt: assetUpdatedAt,
),
createCompanionCallback:
({
required String assetId,
required String checksum,
i0.Value<i2.TrashSyncStatus> status = const i0.Value.absent(),
i0.Value<DateTime?> assetUpdatedAt = const i0.Value.absent(),
}) => i1.TrashSyncEntityCompanion.insert(
assetId: assetId,
checksum: checksum,
status: status,
assetUpdatedAt: assetUpdatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$TrashSyncEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData,
i1.$$TrashSyncEntityTableFilterComposer,
i1.$$TrashSyncEntityTableOrderingComposer,
i1.$$TrashSyncEntityTableAnnotationComposer,
$$TrashSyncEntityTableCreateCompanionBuilder,
$$TrashSyncEntityTableUpdateCompanionBuilder,
(
i1.TrashSyncEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData
>,
),
i1.TrashSyncEntityData,
i0.PrefetchHooks Function()
>;
i0.Index get idxTrashSyncChecksum => i0.Index(
'idx_trash_sync_checksum',
'CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync (checksum)',
);
class $TrashSyncEntityTable extends i3.TrashSyncEntity
with i0.TableInfo<$TrashSyncEntityTable, i1.TrashSyncEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$TrashSyncEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
'assetId',
);
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
'checksum',
);
@override
late final i0.GeneratedColumn<String> checksum = i0.GeneratedColumn<String>(
'checksum',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.TrashSyncStatus, int>
status =
i0.GeneratedColumn<int>(
'status',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: i4.Constant(i2.TrashSyncStatus.pending.index),
).withConverter<i2.TrashSyncStatus>(
i1.$TrashSyncEntityTable.$converterstatus,
);
static const i0.VerificationMeta _assetUpdatedAtMeta =
const i0.VerificationMeta('assetUpdatedAt');
@override
late final i0.GeneratedColumn<DateTime> assetUpdatedAt =
i0.GeneratedColumn<DateTime>(
'asset_updated_at',
aliasedName,
true,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
@override
List<i0.GeneratedColumn> get $columns => [
assetId,
checksum,
status,
assetUpdatedAt,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'trash_sync';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.TrashSyncEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('asset_id')) {
context.handle(
_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
);
} else if (isInserting) {
context.missing(_assetIdMeta);
}
if (data.containsKey('checksum')) {
context.handle(
_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta),
);
} else if (isInserting) {
context.missing(_checksumMeta);
}
if (data.containsKey('asset_updated_at')) {
context.handle(
_assetUpdatedAtMeta,
assetUpdatedAt.isAcceptableOrUnknown(
data['asset_updated_at']!,
_assetUpdatedAtMeta,
),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {assetId};
@override
i1.TrashSyncEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.TrashSyncEntityData(
assetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}asset_id'],
)!,
checksum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}checksum'],
)!,
status: i1.$TrashSyncEntityTable.$converterstatus.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}status'],
)!,
),
assetUpdatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}asset_updated_at'],
),
);
}
@override
$TrashSyncEntityTable createAlias(String alias) {
return $TrashSyncEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.TrashSyncStatus, int, int> $converterstatus =
const i0.EnumIndexConverter<i2.TrashSyncStatus>(
i2.TrashSyncStatus.values,
);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class TrashSyncEntityData extends i0.DataClass
implements i0.Insertable<i1.TrashSyncEntityData> {
final String assetId;
final String checksum;
final i2.TrashSyncStatus status;
final DateTime? assetUpdatedAt;
const TrashSyncEntityData({
required this.assetId,
required this.checksum,
required this.status,
this.assetUpdatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['asset_id'] = i0.Variable<String>(assetId);
map['checksum'] = i0.Variable<String>(checksum);
{
map['status'] = i0.Variable<int>(
i1.$TrashSyncEntityTable.$converterstatus.toSql(status),
);
}
if (!nullToAbsent || assetUpdatedAt != null) {
map['asset_updated_at'] = i0.Variable<DateTime>(assetUpdatedAt);
}
return map;
}
factory TrashSyncEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return TrashSyncEntityData(
assetId: serializer.fromJson<String>(json['assetId']),
checksum: serializer.fromJson<String>(json['checksum']),
status: i1.$TrashSyncEntityTable.$converterstatus.fromJson(
serializer.fromJson<int>(json['status']),
),
assetUpdatedAt: serializer.fromJson<DateTime?>(json['assetUpdatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'assetId': serializer.toJson<String>(assetId),
'checksum': serializer.toJson<String>(checksum),
'status': serializer.toJson<int>(
i1.$TrashSyncEntityTable.$converterstatus.toJson(status),
),
'assetUpdatedAt': serializer.toJson<DateTime?>(assetUpdatedAt),
};
}
i1.TrashSyncEntityData copyWith({
String? assetId,
String? checksum,
i2.TrashSyncStatus? status,
i0.Value<DateTime?> assetUpdatedAt = const i0.Value.absent(),
}) => i1.TrashSyncEntityData(
assetId: assetId ?? this.assetId,
checksum: checksum ?? this.checksum,
status: status ?? this.status,
assetUpdatedAt: assetUpdatedAt.present
? assetUpdatedAt.value
: this.assetUpdatedAt,
);
TrashSyncEntityData copyWithCompanion(i1.TrashSyncEntityCompanion data) {
return TrashSyncEntityData(
assetId: data.assetId.present ? data.assetId.value : this.assetId,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
status: data.status.present ? data.status.value : this.status,
assetUpdatedAt: data.assetUpdatedAt.present
? data.assetUpdatedAt.value
: this.assetUpdatedAt,
);
}
@override
String toString() {
return (StringBuffer('TrashSyncEntityData(')
..write('assetId: $assetId, ')
..write('checksum: $checksum, ')
..write('status: $status, ')
..write('assetUpdatedAt: $assetUpdatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(assetId, checksum, status, assetUpdatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.TrashSyncEntityData &&
other.assetId == this.assetId &&
other.checksum == this.checksum &&
other.status == this.status &&
other.assetUpdatedAt == this.assetUpdatedAt);
}
class TrashSyncEntityCompanion
extends i0.UpdateCompanion<i1.TrashSyncEntityData> {
final i0.Value<String> assetId;
final i0.Value<String> checksum;
final i0.Value<i2.TrashSyncStatus> status;
final i0.Value<DateTime?> assetUpdatedAt;
const TrashSyncEntityCompanion({
this.assetId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.status = const i0.Value.absent(),
this.assetUpdatedAt = const i0.Value.absent(),
});
TrashSyncEntityCompanion.insert({
required String assetId,
required String checksum,
this.status = const i0.Value.absent(),
this.assetUpdatedAt = const i0.Value.absent(),
}) : assetId = i0.Value(assetId),
checksum = i0.Value(checksum);
static i0.Insertable<i1.TrashSyncEntityData> custom({
i0.Expression<String>? assetId,
i0.Expression<String>? checksum,
i0.Expression<int>? status,
i0.Expression<DateTime>? assetUpdatedAt,
}) {
return i0.RawValuesInsertable({
if (assetId != null) 'asset_id': assetId,
if (checksum != null) 'checksum': checksum,
if (status != null) 'status': status,
if (assetUpdatedAt != null) 'asset_updated_at': assetUpdatedAt,
});
}
i1.TrashSyncEntityCompanion copyWith({
i0.Value<String>? assetId,
i0.Value<String>? checksum,
i0.Value<i2.TrashSyncStatus>? status,
i0.Value<DateTime?>? assetUpdatedAt,
}) {
return i1.TrashSyncEntityCompanion(
assetId: assetId ?? this.assetId,
checksum: checksum ?? this.checksum,
status: status ?? this.status,
assetUpdatedAt: assetUpdatedAt ?? this.assetUpdatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
if (status.present) {
map['status'] = i0.Variable<int>(
i1.$TrashSyncEntityTable.$converterstatus.toSql(status.value),
);
}
if (assetUpdatedAt.present) {
map['asset_updated_at'] = i0.Variable<DateTime>(assetUpdatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TrashSyncEntityCompanion(')
..write('assetId: $assetId, ')
..write('checksum: $checksum, ')
..write('status: $status, ')
..write('assetUpdatedAt: $assetUpdatedAt')
..write(')'))
.toString();
}
}

View File

@@ -1,53 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
enum TrashOrigin {
// do not change this order!
localSync,
remoteSync,
localUser,
}
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)')
class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const TrashedLocalAssetEntity();
TextColumn get id => text()();
TextColumn get albumId => text()();
TextColumn get checksum => text().nullable()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
IntColumn get orientation => integer().withDefault(const Constant(0))();
IntColumn get source => intEnum<TrashOrigin>()();
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id, albumId};
}
extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityData {
LocalAsset toLocalAsset() => LocalAsset(
id: id,
name: name,
checksum: checksum,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
width: width,
orientation: orientation,
playbackStyle: playbackStyle,
isEdited: false,
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,18 @@ final class $$UserMetadataEntityTableReferences
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
.createAlias('user_metadata_entity__user_id__user_entity__id');
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity',
)
.userId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$UserEntityTable>('user_entity').id,
),
);
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;

View File

@@ -58,6 +58,12 @@ class DriftBackupRepository extends DriftDatabaseRepository {
INNER JOIN main.local_album_entity la on laa.album_id = la.id
WHERE laa.asset_id = lae.id
AND la.backup_selection = ?3
)
AND NOT EXISTS (
SELECT 1 FROM trash_sync ts WHERE ts.asset_id = lae.id
)
AND NOT EXISTS (
SELECT 1 FROM server_deleted_checksum sd WHERE sd.checksum = lae.checksum
);
''';
@@ -69,7 +75,14 @@ class DriftBackupRepository extends DriftDatabaseRepository {
Variable.withInt(BackupSelection.selected.index),
Variable.withInt(BackupSelection.excluded.index),
],
readsFrom: {_db.localAlbumAssetEntity, _db.localAlbumEntity, _db.localAssetEntity, _db.remoteAssetEntity},
readsFrom: {
_db.localAlbumAssetEntity,
_db.localAlbumEntity,
_db.localAssetEntity,
_db.remoteAssetEntity,
_db.trashSyncEntity,
_db.serverDeletedChecksumEntity,
},
)
.getSingle();
@@ -104,6 +117,16 @@ class DriftBackupRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
),
) &
notExistsQuery(
_db.trashSyncEntity.selectOnly()
..addColumns([_db.trashSyncEntity.assetId])
..where(_db.trashSyncEntity.assetId.equalsExp(lae.id)),
) &
notExistsQuery(
_db.serverDeletedChecksumEntity.selectOnly()
..addColumns([_db.serverDeletedChecksumEntity.checksum])
..where(_db.serverDeletedChecksumEntity.checksum.equalsExp(lae.checksum)),
) &
lae.id.isNotInQuery(_getExcludedSubquery()),
)
..orderBy([(localAsset) => OrderingTerm.desc(localAsset.createdAt)]);

View File

@@ -25,11 +25,11 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
import 'package:immich_mobile/infrastructure/entities/server_deleted_checksum.entity.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart';
@@ -63,7 +63,8 @@ import 'package:sqlite_async/sqlite_async.dart';
PersonEntity,
AssetFaceEntity,
StoreEntity,
TrashedLocalAssetEntity,
TrashSyncEntity,
ServerDeletedChecksumEntity,
AssetEditEntity,
SettingsEntity,
AssetOcrEntity,
@@ -120,7 +121,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 31;
int get schemaVersion => 32;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -221,7 +222,7 @@ class Drift extends $Drift {
await m.alterTable(
TableMigration(
v15.trashedLocalAssetEntity,
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
columnTransformer: {v15.trashedLocalAssetEntity.source: const Constant(0)}, // TrashOrigin.localSync
newColumns: [v15.trashedLocalAssetEntity.source],
),
);
@@ -280,11 +281,7 @@ class Drift extends $Drift {
durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000),
),
);
await trashedLocalAssetEntity.update().write(
TrashedLocalAssetEntityCompanion.custom(
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
),
);
await customStatement('UPDATE trashed_local_asset_entity SET duration_ms = duration_ms * 1000');
},
from23To24: (m, v24) async {
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
@@ -318,6 +315,13 @@ class Drift extends $Drift {
from30To31: (m, v31) async {
await m.createIndex(v31.idxRemoteAssetUploaded);
},
from31To32: (m, v32) async {
await m.createTable(v32.trashSync);
await m.create(v32.idxTrashSyncChecksum);
await m.createTable(v32.serverDeletedChecksum);
await m.create(v32.idxRemoteAssetSoftDeletedChecksum);
await m.deleteTable('trashed_local_asset_entity');
},
),
),
);
@@ -349,6 +353,8 @@ class DriftDatabaseRepository {
const DriftDatabaseRepository(this._db);
Future<T> transaction<T>(Future<T> Function() callback) => _db.transaction(callback);
BaseSelectStatement currentUserIdQuery() => _db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id]);
}
// ignore: invalid_use_of_internal_member

View File

@@ -39,17 +39,19 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
as i18;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i19;
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart'
as i20;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/server_deleted_checksum.entity.drift.dart'
as i21;
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i22;
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
as i23;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
as i24;
import 'package:drift/internal/modular.dart' as i25;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i25;
import 'package:drift/internal/modular.dart' as i26;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -89,19 +91,21 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i18.$AssetFaceEntityTable assetFaceEntity = i18
.$AssetFaceEntityTable(this);
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
.$TrashedLocalAssetEntityTable(this);
late final i21.$AssetEditEntityTable assetEditEntity = i21
late final i20.$TrashSyncEntityTable trashSyncEntity = i20
.$TrashSyncEntityTable(this);
late final i21.$ServerDeletedChecksumEntityTable serverDeletedChecksumEntity =
i21.$ServerDeletedChecksumEntityTable(this);
late final i22.$AssetEditEntityTable assetEditEntity = i22
.$AssetEditEntityTable(this);
late final i22.$SettingsEntityTable settingsEntity = i22.$SettingsEntityTable(
late final i23.$SettingsEntityTable settingsEntity = i23.$SettingsEntityTable(
this,
);
late final i23.$AssetOcrEntityTable assetOcrEntity = i23.$AssetOcrEntityTable(
late final i24.$AssetOcrEntityTable assetOcrEntity = i24.$AssetOcrEntityTable(
this,
);
i24.MergedAssetDrift get mergedAssetDrift => i25.ReadDatabaseContainer(
i25.MergedAssetDrift get mergedAssetDrift => i26.ReadDatabaseContainer(
this,
).accessor<i24.MergedAssetDrift>(i24.MergedAssetDrift.new);
).accessor<i25.MergedAssetDrift>(i25.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -122,6 +126,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
i2.idxRemoteAssetChecksum,
i2.idxRemoteAssetSoftDeletedChecksum,
i2.idxRemoteAssetStackId,
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
i2.idxRemoteAssetUploaded,
@@ -137,7 +142,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
trashSyncEntity,
serverDeletedChecksumEntity,
assetEditEntity,
settingsEntity,
assetOcrEntity,
@@ -150,10 +156,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
i18.idxAssetFacePersonId,
i18.idxAssetFaceAssetId,
i18.idxAssetFaceVisiblePerson,
i20.idxTrashedLocalAssetChecksum,
i20.idxTrashedLocalAssetAlbum,
i21.idxAssetEditAssetId,
i23.idxAssetOcrAssetId,
i20.idxTrashSyncChecksum,
i22.idxAssetEditAssetId,
i24.idxAssetOcrAssetId,
];
@override
i0.StreamQueryUpdateRules
@@ -404,15 +409,18 @@ class $DriftManager {
i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i19.$$StoreEntityTableTableManager get storeEntity =>
i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
i20.$$TrashedLocalAssetEntityTableTableManager(
i20.$$TrashSyncEntityTableTableManager get trashSyncEntity =>
i20.$$TrashSyncEntityTableTableManager(_db, _db.trashSyncEntity);
i21.$$ServerDeletedChecksumEntityTableTableManager
get serverDeletedChecksumEntity =>
i21.$$ServerDeletedChecksumEntityTableTableManager(
_db,
_db.trashedLocalAssetEntity,
_db.serverDeletedChecksumEntity,
);
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
i22.$$SettingsEntityTableTableManager get settingsEntity =>
i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
i23.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
i23.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
i22.$$AssetEditEntityTableTableManager get assetEditEntity =>
i22.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
i23.$$SettingsEntityTableTableManager get settingsEntity =>
i23.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
i24.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
i24.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
}

View File

@@ -16506,6 +16506,632 @@ final class Schema31 extends i0.VersionedSchema {
);
}
final class Schema32 extends i0.VersionedSchema {
Schema32({required super.database}) : super(version: 32);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxLocalAssetCreatedAt,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetSoftDeletedChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
idxRemoteAssetUploaded,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashSync,
serverDeletedChecksum,
assetEditEntity,
settings,
assetOcrEntity,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashSyncChecksum,
idxAssetEditAssetId,
idxAssetOcrAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxLocalAssetCreatedAt = i1.Index(
'idx_local_asset_created_at',
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetSoftDeletedChecksum = i1.Index(
'idx_remote_asset_soft_deleted_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_soft_deleted_checksum ON remote_asset_entity (checksum) WHERE deleted_at IS NOT NULL',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
final i1.Index idxRemoteAssetUploaded = i1.Index(
'idx_remote_asset_uploaded',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape52 trashSync = Shape52(
source: i0.VersionedTable(
entityName: 'trash_sync',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [_column_225, _column_119, _column_226, _column_227],
attachedDatabase: database,
),
alias: null,
);
late final Shape53 serverDeletedChecksum = Shape53(
source: i0.VersionedTable(
entityName: 'server_deleted_checksum',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(checksum)'],
columns: [_column_119],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 settings = Shape49(
source: i0.VersionedTable(
entityName: 'settings',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_224, _column_115],
attachedDatabase: database,
),
alias: null,
);
late final Shape51 assetOcrEntity = Shape51(
source: i0.VersionedTable(
entityName: 'asset_ocr_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_213,
_column_214,
_column_215,
_column_216,
_column_217,
_column_218,
_column_219,
_column_220,
_column_221,
_column_222,
_column_223,
_column_201,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashSyncChecksum = i1.Index(
'idx_trash_sync_checksum',
'CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync (checksum)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
final i1.Index idxAssetOcrAssetId = i1.Index(
'idx_asset_ocr_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
);
}
class Shape52 extends i0.VersionedTable {
Shape52({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get status =>
columnsByName['status']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get assetUpdatedAt =>
columnsByName['asset_updated_at']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_225(String aliasedName) =>
i1.GeneratedColumn<String>(
'asset_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<int> _column_226(String aliasedName) =>
i1.GeneratedColumn<int>(
'status',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_227(String aliasedName) =>
i1.GeneratedColumn<String>(
'asset_updated_at',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
class Shape53 extends i0.VersionedTable {
Shape53({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -16537,6 +17163,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -16690,6 +17317,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from30To31(migrator, schema);
return 31;
case 31:
final schema = Schema32(database: database);
final migrator = i1.Migrator(database, schema);
await from31To32(migrator, schema);
return 32;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -16727,6 +17359,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -16759,5 +17392,6 @@ i1.OnUpgrade stepByStep({
from28To29: from28To29,
from29To30: from29To30,
from30To31: from30To31,
from31To32: from31To32,
),
);

View File

@@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:flutter/foundation.dart';
class SettingsRepository extends DriftDatabaseRepository {
final Drift _db;
@@ -31,6 +32,9 @@ class SettingsRepository extends DriftDatabaseRepository {
return _instance!;
}
@visibleForTesting
static void reset() => _instance = null;
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
Future<void> clear(Iterable<SettingsKey> keys) async {

View File

@@ -71,6 +71,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
await _db.assetOcrEntity.deleteAll();
await _db.trashSyncEntity.deleteAll();
await _db.serverDeletedChecksumEntity.deleteAll();
});
} finally {
// re-enable FK even if the transaction throws, otherwise the connection

View File

@@ -0,0 +1,280 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/server_deleted_checksum.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftTrashSyncRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftTrashSyncRepository(this._db) : super(_db);
// Prunes asset markers for assets that are now live on the server
Future<void> pruneStaleMarkers() async {
final liveChecksums = _db.selectOnly(_db.remoteAssetEntity)
..addColumns([_db.remoteAssetEntity.checksum])
..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.isInQuery(currentUserIdQuery()));
await _db.transaction(() async {
await (_db.delete(_db.serverDeletedChecksumEntity)..where((t) => t.checksum.isInQuery(liveChecksums))).go();
await (_db.delete(
_db.trashSyncEntity,
)..where((t) => t.checksum.isInQuery(liveChecksums) & t.status.equalsValue(.pending))).go();
});
}
// Prunes dismissed assets marker for assets that are not on the server anymore
Future<void> pruneDismissedMarkers() async {
final softDeletedChecksums = _db.selectOnly(_db.remoteAssetEntity)
..addColumns([_db.remoteAssetEntity.checksum])
..where(
_db.remoteAssetEntity.deletedAt.isNotNull() & _db.remoteAssetEntity.ownerId.isInQuery(currentUserIdQuery()),
);
final serverDeletedChecksums = _db.selectOnly(_db.serverDeletedChecksumEntity)
..addColumns([_db.serverDeletedChecksumEntity.checksum]);
await (_db.delete(_db.trashSyncEntity)..where(
(t) =>
t.status.equalsValue(.dismissed) &
t.checksum.isNotInQuery(softDeletedChecksums) &
t.checksum.isNotInQuery(serverDeletedChecksums),
))
.go();
}
// Prunes pending markers for assets that has been modified locally
Future<void> prunePendingMarkers() async {
final staleIds = _db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.assetId])
..join([
innerJoin(
_db.localAssetEntity,
_db.localAssetEntity.id.equalsExp(_db.trashSyncEntity.assetId),
useColumns: false,
),
])
..where(
_db.trashSyncEntity.status.equalsValue(.pending) &
_db.localAssetEntity.checksum.isNotNull() &
_db.localAssetEntity.checksum.equalsExp(_db.trashSyncEntity.checksum).not(),
);
await (_db.delete(_db.trashSyncEntity)..where((t) => t.assetId.isInQuery(staleIds))).go();
}
Future<void> recordHardDeletedChecksums(Iterable<String> remoteIds) async {
for (final slice in remoteIds.toSet().slices(kDriftMaxChunk)) {
final checksums =
await (_db.selectOnly(_db.remoteAssetEntity, distinct: true)
..addColumns([_db.remoteAssetEntity.checksum])
..where(
_db.remoteAssetEntity.id.isIn(slice) & _db.remoteAssetEntity.ownerId.isInQuery(currentUserIdQuery()),
))
.map((r) => r.read(_db.remoteAssetEntity.checksum)!)
.get();
if (checksums.isEmpty) {
continue;
}
await _db.batch((batch) {
for (final checksum in checksums) {
batch.insert(
_db.serverDeletedChecksumEntity,
ServerDeletedChecksumEntityCompanion.insert(checksum: checksum),
onConflict: DoNothing(),
);
}
});
}
}
Future<void> recordSoftDeleteAssets() => _recordAssets(
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) &
_db.remoteAssetEntity.deletedAt.isNotNull() &
_db.remoteAssetEntity.ownerId.isInQuery(currentUserIdQuery()),
useColumns: false,
),
);
Future<void> recordHardDeletedAssets() => _recordAssets(
innerJoin(
_db.serverDeletedChecksumEntity,
_db.serverDeletedChecksumEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
);
Future<void> _recordAssets(Join contentJoin) async {
final excludedAssetIds = _db.selectOnly(_db.localAlbumAssetEntity)
..addColumns([_db.localAlbumAssetEntity.assetId])
..join([
innerJoin(
_db.localAlbumEntity,
_db.localAlbumEntity.id.equalsExp(_db.localAlbumAssetEntity.albumId),
useColumns: false,
),
])
..where(_db.localAlbumEntity.backupSelection.equalsValue(.excluded));
final selectedAssetsQuery = _db.selectOnly(_db.localAlbumAssetEntity)
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id) &
_db.localAlbumAssetEntity.albumId.isInQuery(
_db.selectOnly(_db.localAlbumEntity)
..addColumns([_db.localAlbumEntity.id])
..where(_db.localAlbumEntity.backupSelection.equalsValue(.selected)),
),
);
final dismissedAssetsQuery = _db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.assetId])
..where(
_db.trashSyncEntity.checksum.equalsExp(_db.localAssetEntity.checksum) &
_db.trashSyncEntity.status.equalsValue(.dismissed),
);
final source = _db.selectOnly(_db.localAssetEntity)
..addColumns([_db.localAssetEntity.id, _db.localAssetEntity.checksum, _db.localAssetEntity.updatedAt])
..join([contentJoin])
..where(
_db.localAssetEntity.checksum.isNotNull() &
existsQuery(selectedAssetsQuery) &
_db.localAssetEntity.id.isNotInQuery(excludedAssetIds) &
notExistsQuery(dismissedAssetsQuery),
);
await _db
.into(_db.trashSyncEntity)
.insertFromSelect(
source,
columns: {
_db.trashSyncEntity.assetId: _db.localAssetEntity.id,
_db.trashSyncEntity.checksum: _db.localAssetEntity.checksum,
_db.trashSyncEntity.assetUpdatedAt: _db.localAssetEntity.updatedAt,
},
mode: .insertOrIgnore,
);
}
Future<void> markTrashed(Iterable<String> assetIds) async {
final set = assetIds.toSet();
if (set.isEmpty) {
return;
}
await _db.transaction(() async {
for (final slice in set.slices(kDriftMaxChunk)) {
await (_db.update(
_db.trashSyncEntity,
)..where((t) => t.assetId.isIn(slice))).write(const TrashSyncEntityCompanion(status: .new(.trashed)));
await (_db.delete(_db.localAssetEntity)..where((t) => t.id.isIn(slice))).go();
}
});
}
Future<void> markRestored(Iterable<String> assetIds) async {
final set = assetIds.toSet();
if (set.isEmpty) {
return;
}
for (final slice in set.slices(kDriftMaxChunk)) {
await (_db.update(
_db.trashSyncEntity,
)..where((t) => t.assetId.isIn(slice))).write(const TrashSyncEntityCompanion(status: .new(.restored)));
}
}
Future<void> restoreChecksums() async {
final restored = await (_db.select(_db.trashSyncEntity)..where((t) => t.status.equalsValue(.restored))).get();
if (restored.isEmpty) {
return;
}
await _db.batch((batch) {
for (final row in restored) {
final assetUpdatedAt = row.assetUpdatedAt;
if (assetUpdatedAt == null) {
continue;
}
batch.update(
_db.localAssetEntity,
LocalAssetEntityCompanion(checksum: .new(row.checksum)),
where: (t) => t.id.equals(row.assetId) & t.checksum.isNull() & t.updatedAt.equals(assetUpdatedAt),
);
}
batch.deleteWhere(_db.trashSyncEntity, (t) => t.status.equalsValue(.restored));
});
}
// Mark assets that were previously marked as trashed but are now live on the device as dismissed
Future<void> reconcileTrashed(Iterable<String> assetIds) async {
final set = assetIds.toSet();
if (set.isEmpty) {
return;
}
JoinedSelectStatement localAssetQuery($TrashSyncEntityTable trash) => _db.selectOnly(_db.localAssetEntity)
..addColumns([_db.localAssetEntity.id])
..where(_db.localAssetEntity.id.equalsExp(trash.assetId));
await _db.transaction(() async {
for (final slice in set.slices(kDriftMaxChunk)) {
await (_db.update(_db.trashSyncEntity)..where((t) => t.assetId.isIn(slice) & existsQuery(localAssetQuery(t))))
.write(const TrashSyncEntityCompanion(status: .new(.dismissed)));
await (_db.delete(
_db.trashSyncEntity,
)..where((t) => t.assetId.isIn(slice) & notExistsQuery(localAssetQuery(t)))).go();
}
});
}
Future<void> deleteMarkers(Iterable<String> assetIds) async {
final set = assetIds.toSet();
if (set.isEmpty) {
return;
}
for (final slice in set.slices(kDriftMaxChunk)) {
await (_db.delete(_db.trashSyncEntity)..where((t) => t.assetId.isIn(slice))).go();
}
}
Future<List<String>> getPendingAssetIds() =>
_trashSyncAssetIdsWhere(_db.trashSyncEntity.status.equalsValue(.pending));
Future<List<String>> getTrashedAssetIds() =>
_trashSyncAssetIdsWhere(_db.trashSyncEntity.status.equalsValue(.trashed));
Future<List<String>> _trashSyncAssetIdsWhere(Expression<bool> filter) {
return (_db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.assetId])
..where(filter))
.map((row) => row.read(_db.trashSyncEntity.assetId)!)
.get();
}
Future<List<String>> getRestorableAssetIds() {
return (_db.selectOnly(_db.trashSyncEntity, distinct: true)
..addColumns([_db.trashSyncEntity.assetId])
..join([
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.trashSyncEntity.checksum),
useColumns: false,
),
])
..where(
_db.trashSyncEntity.status.equalsValue(.trashed) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.isInQuery(currentUserIdQuery()),
))
.map((row) => row.read(_db.trashSyncEntity.assetId)!)
.get();
}
}

View File

@@ -1,311 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
typedef TrashedAsset = ({String albumId, LocalAsset asset});
class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
Future<void> updateHashes(Map<String, String> hashes) {
if (hashes.isEmpty) {
return Future.value();
}
return _db.batch((batch) async {
for (final entry in hashes.entries) {
batch.update(
_db.trashedLocalAssetEntity,
TrashedLocalAssetEntityCompanion(checksum: Value(entry.value)),
where: (e) => e.id.equals(entry.key),
);
}
});
}
Future<List<LocalAsset>> getAssetsToHash(Iterable<String> albumIds) {
final query = _db.trashedLocalAssetEntity.select()..where((r) => r.albumId.isIn(albumIds) & r.checksum.isNull());
return query.map((row) => row.toLocalAsset()).get();
}
Future<Iterable<LocalAsset>> getToRestore() async {
final selectedAlbumIds = (_db.selectOnly(_db.localAlbumEntity)
..addColumns([_db.localAlbumEntity.id])
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected)));
final rows =
await (_db.select(_db.trashedLocalAssetEntity).join([
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum),
),
])..where(
_db.trashedLocalAssetEntity.source.equalsValue(TrashOrigin.remoteSync) &
_db.trashedLocalAssetEntity.albumId.isInQuery(selectedAlbumIds) &
_db.remoteAssetEntity.deletedAt.isNull(),
))
.get();
return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toLocalAsset());
}
/// Applies resulted snapshot of trashed assets:
/// - upserts incoming rows
/// - deletes rows that are not present in the snapshot
Future<void> processTrashSnapshot(Iterable<TrashedAsset> trashedAssets) async {
if (trashedAssets.isEmpty) {
await _db.delete(_db.trashedLocalAssetEntity).go();
return;
}
final assetIds = trashedAssets.map((e) => e.asset.id).toSet();
Map<String, String> localChecksumById = await _getCachedChecksums(assetIds);
return _db.transaction(() async {
await _db.batch((batch) {
for (final item in trashedAssets) {
final effectiveChecksum = localChecksumById[item.asset.id] ?? item.asset.checksum;
final companion = TrashedLocalAssetEntityCompanion.insert(
id: item.asset.id,
albumId: item.albumId,
checksum: Value(effectiveChecksum),
name: item.asset.name,
type: item.asset.type,
createdAt: Value(item.asset.createdAt),
updatedAt: Value(item.asset.updatedAt),
width: Value(item.asset.width),
height: Value(item.asset.height),
durationMs: Value(item.asset.durationMs),
isFavorite: Value(item.asset.isFavorite),
orientation: Value(item.asset.orientation),
playbackStyle: Value(item.asset.playbackStyle),
source: TrashOrigin.localSync,
);
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
_db.trashedLocalAssetEntity,
companion,
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(item.asset.updatedAt)),
);
}
});
if (assetIds.length <= kDriftMaxChunk) {
await (_db.delete(_db.trashedLocalAssetEntity)..where((row) => row.id.isNotIn(assetIds))).go();
} else {
final existingIds = await (_db.selectOnly(
_db.trashedLocalAssetEntity,
)..addColumns([_db.trashedLocalAssetEntity.id])).map((r) => r.read(_db.trashedLocalAssetEntity.id)!).get();
final idToDelete = existingIds.where((id) => !assetIds.contains(id));
for (final slice in idToDelete.slices(kDriftMaxChunk)) {
await (_db.delete(_db.trashedLocalAssetEntity)..where((t) => t.id.isIn(slice))).go();
}
}
});
}
Stream<int> watchCount() {
return (_db.selectOnly(_db.trashedLocalAssetEntity)..addColumns([_db.trashedLocalAssetEntity.id.count()]))
.watchSingle()
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
}
Stream<int> watchHashedCount() {
return (_db.selectOnly(_db.trashedLocalAssetEntity)
..addColumns([_db.trashedLocalAssetEntity.id.count()])
..where(_db.trashedLocalAssetEntity.checksum.isNotNull()))
.watchSingle()
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
}
Future<void> trashLocalAsset(Map<String, List<LocalAsset>> assetsByAlbums) async {
if (assetsByAlbums.isEmpty) {
return Future.value();
}
final companions = <TrashedLocalAssetEntityCompanion>[];
final idToDelete = <String>{};
for (final entry in assetsByAlbums.entries) {
for (final asset in entry.value) {
idToDelete.add(asset.id);
companions.add(
TrashedLocalAssetEntityCompanion(
id: Value(asset.id),
name: Value(asset.name),
albumId: Value(entry.key),
checksum: Value(asset.checksum),
type: Value(asset.type),
width: Value(asset.width),
height: Value(asset.height),
durationMs: Value(asset.durationMs),
isFavorite: Value(asset.isFavorite),
orientation: Value(asset.orientation),
playbackStyle: Value(asset.playbackStyle),
createdAt: Value(asset.createdAt),
updatedAt: Value(asset.updatedAt),
source: const Value(TrashOrigin.remoteSync),
),
);
}
}
await _db.transaction(() async {
for (final companion in companions) {
await _db.into(_db.trashedLocalAssetEntity).insertOnConflictUpdate(companion);
}
for (final slice in idToDelete.slices(kDriftMaxChunk)) {
await (_db.delete(_db.localAssetEntity)..where((t) => t.id.isIn(slice))).go();
}
});
}
Future<void> applyRestoredAssets(List<String> idList) async {
if (idList.isEmpty) {
return Future.value();
}
final trashedAssets = <TrashedLocalAssetEntityData>[];
for (final slice in idList.slices(kDriftMaxChunk)) {
final q = _db.select(_db.trashedLocalAssetEntity)..where((t) => t.id.isIn(slice));
trashedAssets.addAll(await q.get());
}
if (trashedAssets.isEmpty) {
return;
}
final companions = trashedAssets.map((e) {
return LocalAssetEntityCompanion.insert(
id: e.id,
name: e.name,
type: e.type,
createdAt: Value(e.createdAt),
updatedAt: Value(e.updatedAt),
width: Value(e.width),
height: Value(e.height),
durationMs: Value(e.durationMs),
checksum: Value(e.checksum),
isFavorite: Value(e.isFavorite),
orientation: Value(e.orientation),
playbackStyle: Value(e.playbackStyle),
);
});
await _db.transaction(() async {
for (final companion in companions) {
await _db.into(_db.localAssetEntity).insertOnConflictUpdate(companion);
}
for (final slice in idList.slices(kDriftMaxChunk)) {
await (_db.delete(_db.trashedLocalAssetEntity)..where((t) => t.id.isIn(slice))).go();
}
});
}
Future<void> applyTrashedAssets(List<String> idList) async {
if (idList.isEmpty) {
return Future.value();
}
final trashedAssets = <({LocalAssetEntityData asset, String albumId})>[];
for (final slice in idList.slices(kDriftMaxChunk)) {
final rows = await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
])..where(_db.localAlbumAssetEntity.assetId.isIn(slice))).get();
final assetsWithAlbum = rows.map(
(row) =>
(albumId: row.readTable(_db.localAlbumAssetEntity).albumId, asset: row.readTable(_db.localAssetEntity)),
);
trashedAssets.addAll(assetsWithAlbum);
}
if (trashedAssets.isEmpty) {
return;
}
final companions = trashedAssets.map((e) {
return TrashedLocalAssetEntityCompanion.insert(
id: e.asset.id,
name: e.asset.name,
type: e.asset.type,
createdAt: Value(e.asset.createdAt),
updatedAt: Value(e.asset.updatedAt),
width: Value(e.asset.width),
height: Value(e.asset.height),
durationMs: Value(e.asset.durationMs),
checksum: Value(e.asset.checksum),
isFavorite: Value(e.asset.isFavorite),
orientation: Value(e.asset.orientation),
playbackStyle: Value(e.asset.playbackStyle),
source: TrashOrigin.localUser,
albumId: e.albumId,
);
});
await _db.transaction(() async {
for (final companion in companions) {
await _db.into(_db.trashedLocalAssetEntity).insertOnConflictUpdate(companion);
}
for (final slice in idList.slices(kDriftMaxChunk)) {
await (_db.delete(_db.localAssetEntity)..where((t) => t.id.isIn(slice))).go();
}
});
}
Future<Map<String, List<LocalAsset>>> getToTrash() async {
final result = <String, List<LocalAsset>>{};
final rows =
await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
),
])..where(
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
_db.remoteAssetEntity.deletedAt.isNotNull(),
))
.get();
for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final asset = row.readTable(_db.localAssetEntity).toDto();
(result[albumId] ??= <LocalAsset>[]).add(asset);
}
return result;
}
//attempt to reuse existing checksums
Future<Map<String, String>> _getCachedChecksums(Set<String> assetIds) async {
final localChecksumById = <String, String>{};
for (final slice in assetIds.slices(kDriftMaxChunk)) {
final rows =
await (_db.selectOnly(_db.localAssetEntity)
..where(_db.localAssetEntity.id.isIn(slice) & _db.localAssetEntity.checksum.isNotNull())
..addColumns([_db.localAssetEntity.id, _db.localAssetEntity.checksum]))
.get();
for (final r in rows) {
localChecksumById[r.read(_db.localAssetEntity.id)!] = r.read(_db.localAssetEntity.checksum)!;
}
}
return localChecksumById;
}
}

View File

@@ -333,6 +333,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
// TODO: Bring back when the soft freeze issue is addressed
// backgroundManager.syncCloudIds(),
]);
await backgroundManager.syncTrash();
} else {
await backgroundManager.hashAssets();
}

View File

@@ -132,10 +132,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
textInputAction: TextInputAction.done,
autofocus: false,
decoration: InputDecoration(
labelText: slugController.text.isNotEmpty ? context.t.shared_link_custom_url_title : null,
labelText: slugController.text.isNotEmpty ? context.t.custom_url : null,
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
border: const OutlineInputBorder(),
hintText: context.t.shared_link_custom_url_title,
hintText: context.t.custom_url,
prefixText: slugController.text.isNotEmpty ? '/s/' : null,
prefixStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),

228
mobile/lib/platform/asset_media_api.g.dart generated Normal file
View File

@@ -0,0 +1,228 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: unused_import, unused_shown_name
// ignore_for_file: type=lint
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
} else if (replyList.length > 1) {
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
}
return replyList.firstOrNull;
}
bool _deepEquals(Object? a, Object? b) {
if (identical(a, b)) {
return true;
}
if (a is double && b is double) {
if (a.isNaN && b.isNaN) {
return true;
}
return a == b;
}
if (a is List && b is List) {
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
if (a.length != b.length) {
return false;
}
for (final MapEntry<Object?, Object?> entryA in a.entries) {
bool found = false;
for (final MapEntry<Object?, Object?> entryB in b.entries) {
if (_deepEquals(entryA.key, entryB.key)) {
if (_deepEquals(entryA.value, entryB.value)) {
found = true;
break;
} else {
return false;
}
}
}
if (!found) {
return false;
}
}
return true;
}
return a == b;
}
int _deepHash(Object? value) {
if (value is List) {
return Object.hashAll(value.map(_deepHash));
}
if (value is Map) {
int result = 0;
for (final MapEntry<Object?, Object?> entry in value.entries) {
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
}
return result;
}
if (value is double && value.isNaN) {
// Normalize NaN to a consistent hash.
return 0x7FF8000000000000.hashCode;
}
if (value is double && value == 0.0) {
// Normalize -0.0 to 0.0 so they have the same hash code.
return 0.0.hashCode;
}
return value.hashCode;
}
enum AssetMediaActionStatus { done, alreadyInState, notFound, failed }
class AssetMediaActionResult {
AssetMediaActionResult({required this.id, required this.status});
String id;
AssetMediaActionStatus status;
List<Object?> _toList() {
return <Object?>[id, status];
}
Object encode() {
return _toList();
}
static AssetMediaActionResult decode(Object result) {
result as List<Object?>;
return AssetMediaActionResult(id: result[0]! as String, status: result[1]! as AssetMediaActionStatus);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! AssetMediaActionResult || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(id, other.id) && _deepEquals(status, other.status);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is AssetMediaActionStatus) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is AssetMediaActionResult) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final value = readValue(buffer) as int?;
return value == null ? null : AssetMediaActionStatus.values[value];
case 130:
return AssetMediaActionResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class AssetMediaApi {
/// Constructor for [AssetMediaApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
AssetMediaApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<List<AssetMediaActionResult>> trash(List<String> ids) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.AssetMediaApi.trash$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[ids]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return (pigeonVar_replyValue! as List<Object?>).cast<AssetMediaActionResult>();
}
Future<List<AssetMediaActionResult>> restore(List<String> ids) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.AssetMediaApi.restore$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[ids]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return (pigeonVar_replyValue! as List<Object?>).cast<AssetMediaActionResult>();
}
Future<List<String>> trashedAmong(List<String> ids) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.AssetMediaApi.trashedAmong$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[ids]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
}
}

View File

@@ -649,44 +649,6 @@ class NativeSyncApi {
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
}
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
}
Future<bool> restoreFromTrashById(String mediaId, int type) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[mediaId, type]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';

View File

@@ -3,23 +3,25 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/actions/action.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/utils/asset_filter.dart';
import 'package:immich_ui/immich_ui.dart';
class FavoriteAction extends AssetAction<RemoteAsset> {
final bool favorite;
final bool shouldFavorite;
FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite);
FavoriteAction({required super.assets}) : shouldFavorite = assets.any((asset) => !asset.isFavorite);
@override
IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
IconData get icon => shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
@override
String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite;
String label(ActionScope scope) => shouldFavorite ? scope.context.t.favorite : scope.context.t.unfavorite;
@override
Iterable<RemoteAsset> filter(ActionScope scope) =>
AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite);
Iterable<RemoteAsset> filter(ActionScope scope) => assets
.where(
(asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isFavorite == !shouldFavorite,
)
.cast<RemoteAsset>();
@override
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
@@ -29,8 +31,8 @@ class FavoriteAction extends AssetAction<RemoteAsset> {
final ActionScope(:ref) = scope;
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).updateFavorite(assets, favorite);
final message = favorite
await ref.read(assetServiceProvider).updateFavorite(assets, shouldFavorite);
final message = shouldFavorite
? StaticTranslations.instance.favorite_action_prompt(count: assets.length)
: StaticTranslations.instance.unfavorite_action_prompt(count: assets.length);
snackbar.success(message);

View File

@@ -63,7 +63,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)),
ViewerKebabMenu(originalTheme: originalTheme),
];
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];

View File

@@ -45,6 +45,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
completer.operation.valueOrCancellation().whenComplete(() {
cachedStream.removeListener(listener);
cachedOperation = null;
});
cachedOperation = completer.operation;
return null;
@@ -94,9 +95,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
isFinished = isFinal;
return codec;
} catch (e) {
if (isCancelled) {
return null;
}
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
@@ -108,33 +106,18 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
}
}
Stream<ImageInfo> initialImageStream({required bool isFinal}) async* {
Stream<ImageInfo> initialImageStream() async* {
final cachedOperation = this.cachedOperation;
if (isCancelled) {
return;
}
if (cachedOperation == null) {
// image resolved synchronously
isFinished = isFinal;
return;
}
try {
final cachedImage = await cachedOperation.valueOrCancellation();
if (isCancelled || cachedImage == null) {
return;
if (cachedImage != null && !isCancelled) {
yield cachedImage;
}
isFinished = isFinal;
yield cachedImage;
} catch (e, stack) {
if (isCancelled) {
return;
}
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
_log.severe('Error loading initial image', e, stack);
} finally {
this.cachedOperation = null;

Some files were not shown because too many files have changed in this diff Show More