Compare commits

..

9 Commits

Author SHA1 Message Date
Alex
d261339f66 wip 2026-07-15 14:57:26 -05:00
Alex
ba5651c1a2 wip 2026-07-15 12:15:42 -05:00
bo0tzz
2237078bd5 chore: bump create-workflow-token action to v3 (#29571) 2026-07-15 14:33:23 +02:00
Jorge Montejo
55dc579059 feat(web): Keep show more open on duplicates (#29734)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-15 11:22:36 +00:00
Raul
b4530b0671 fix(server): workflow date filter, make end date inclusive (#29876)
Co-authored-by: Raul Plesa <raul.plesa@external.grifols.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-15 10:50:03 +00:00
Lorenzo Montanari
a04e2ddeca fix(server): updated default CSP config to support videos from V3 player (#29830) 2026-07-15 12:49:18 +02:00
Kobi Hikri
5f2e8a4fcf chore(security.txt): bump expired Expires field (RFC 9116) (#29932) 2026-07-15 10:19:44 +00:00
Priyanshu
dc55b234d9 fix(web): use Container component for responsive admin maintenance la… (#29917)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-15 12:18:55 +02:00
Priyanshu
f6bf22d9ca fix(web): hide stack thumbnail tray in slideshow mode (#29918)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-15 12:10:35 +02:00
213 changed files with 4489 additions and 17753 deletions

View File

@@ -51,10 +51,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -79,10 +80,12 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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:
@@ -202,10 +205,11 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +19,12 @@ jobs:
actions: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +31,11 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -63,10 +64,11 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +44,11 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +23,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +21,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -54,10 +55,11 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +20,12 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -126,10 +128,13 @@ 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@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +17,12 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +15,12 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,6 +35,8 @@ 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,10 +14,11 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +54,11 @@ 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@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -135,6 +136,8 @@ 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,10 +14,11 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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:
@@ -32,10 +33,11 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +16,11 @@ jobs:
packages: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +20,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -49,10 +50,11 @@ jobs:
working-directory: ./mobile
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +17,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -77,10 +78,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -105,10 +107,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -136,10 +139,11 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -167,10 +171,11 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -211,10 +216,11 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -249,10 +255,11 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -277,10 +284,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -327,10 +335,11 @@ jobs:
working-directory: ./e2e
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -359,10 +368,11 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -395,10 +405,11 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -472,10 +483,11 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -580,10 +592,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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:
@@ -618,10 +631,11 @@ jobs:
working-directory: ./machine-learning
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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:
@@ -648,10 +662,11 @@ jobs:
working-directory: ./.github
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -678,10 +693,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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:
@@ -699,10 +715,11 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -757,10 +774,11 @@ jobs:
- 5432:5432
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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,10 +24,11 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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
@@ -47,10 +48,11 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
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

@@ -557,6 +557,20 @@
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_title": "Sign out",
"app_download_links": "App Download Links",
"app_icon_blossom": "Blossom",
"app_icon_change_failed": "Unable to change the app icon",
"app_icon_classic": "Classic",
"app_icon_forest": "Forest",
"app_icon_gold": "Gold",
"app_icon_ink": "Ink",
"app_icon_midnight": "Midnight",
"app_icon_neon": "Neon",
"app_icon_ocean": "Ocean",
"app_icon_retro": "Retro",
"app_icon_scenic": "Scenic",
"app_icon_subtitle": "Choose the icon shown on your home screen",
"app_icon_sunset": "Sunset",
"app_icon_title": "App Icon",
"app_settings": "App Settings",
"app_stores": "App Stores",
"app_update_available": "App update is available",

View File

@@ -56,11 +56,6 @@
Window background behind the Flutter UI. -->
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!--TODO:
Add this filter if you want to handle shared images-->
@@ -137,6 +132,86 @@
</intent-filter>
</activity>
<!-- Launcher aliases for the selectable app icons (see AppIconApiImpl).
Exactly one is enabled at a time; the LAUNCHER intent-filter lives on
these instead of MainActivity so the icon can be swapped at runtime. -->
<activity-alias android:name=".AppIconClassic" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconRetro" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_retro" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconScenic" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_scenic" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconMidnight" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_midnight" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconOcean" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_ocean" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconSunset" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_sunset" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconForest" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_forest" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconBlossom" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_blossom" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconInk" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_ink" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconNeon" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_neon" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias android:name=".AppIconGold" android:targetActivity=".MainActivity"
android:icon="@mipmap/ic_launcher_gold" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"

View File

@@ -4,6 +4,8 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.ext.SdkExtensions
import app.alextran.immich.appicon.AppIconApi
import app.alextran.immich.appicon.AppIconApiImpl
import app.alextran.immich.background.BackgroundEngineLock
import app.alextran.immich.background.BackgroundWorkerApiImpl
import app.alextran.immich.background.BackgroundWorkerFgHostApi
@@ -18,7 +20,6 @@ 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
@@ -62,12 +63,12 @@ class MainActivity : FlutterFragmentActivity() {
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
AppIconApi.setUp(messenger, AppIconApiImpl(ctx))
flutterEngine.plugins.add(ViewIntentPlugin())
flutterEngine.plugins.add(backgroundEngineLockImpl)
flutterEngine.plugins.add(nativeSyncApiImpl)
flutterEngine.plugins.add(permissionApiImpl)
flutterEngine.plugins.add(AssetMediaApiImpl(ctx))
}
fun cancelPlugins(flutterEngine: FlutterEngine) {
@@ -77,8 +78,6 @@ 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

@@ -0,0 +1,110 @@
// 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.appicon
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 AppIconPigeonUtils {
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)
)
}
}
}
/**
* 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()
private open class AppIconPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
super.writeValue(stream, value)
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface AppIconApi {
fun setAppIcon(iconId: String, callback: (Result<Unit>) -> Unit)
fun getAppIcon(): String
companion object {
/** The codec used by AppIconApi. */
val codec: MessageCodec<Any?> by lazy {
AppIconPigeonCodec()
}
/** Sets up an instance of `AppIconApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: AppIconApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AppIconApi.setAppIcon$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val iconIdArg = args[0] as String
api.setAppIcon(iconIdArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(AppIconPigeonUtils.wrapError(error))
} else {
reply.reply(AppIconPigeonUtils.wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AppIconApi.getAppIcon$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getAppIcon())
} catch (exception: Throwable) {
AppIconPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
package app.alextran.immich.appicon
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
/**
* Switches the launcher icon by toggling the `.AppIcon*` activity-aliases
* declared in the AndroidManifest. Exactly one alias is enabled at a time;
* the aliases all target MainActivity.
*/
class AppIconApiImpl(context: Context) : AppIconApi {
private val ctx: Context = context.applicationContext
companion object {
private const val DEFAULT_ICON = "classic"
private val ICONS =
listOf("classic", "retro", "scenic", "midnight", "ocean", "sunset", "forest", "blossom", "ink", "neon", "gold")
}
private fun componentFor(iconId: String): ComponentName {
val alias = "AppIcon" + iconId.replaceFirstChar { it.uppercase() }
return ComponentName(ctx.packageName, "app.alextran.immich.$alias")
}
override fun setAppIcon(iconId: String, callback: (Result<Unit>) -> Unit) {
if (iconId !in ICONS) {
callback(Result.failure(FlutterError("invalid_icon", "Unknown app icon: $iconId")))
return
}
val pm = ctx.packageManager
for (icon in ICONS) {
val state = if (icon == iconId) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
pm.setComponentEnabledSetting(componentFor(icon), state, PackageManager.DONT_KILL_APP)
}
callback(Result.success(Unit))
}
override fun getAppIcon(): String {
val pm = ctx.packageManager
for (icon in ICONS) {
if (pm.getComponentEnabledSetting(componentFor(icon)) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
) {
return icon
}
}
return DEFAULT_ICON
}
}

View File

@@ -2,30 +2,17 @@ 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 supervisor = SupervisorJob()
protected var scope = CoroutineScope(supervisor + Dispatchers.Default)
private set
private var detached: Boolean = false;
@CallSuper
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
detached = false
if (!scope.isActive) {
supervisor = SupervisorJob()
scope = CoroutineScope(supervisor + Dispatchers.Default)
}
detached = false;
}
fun detachFromEngine() {
detached = true
supervisor.cancel()
}
@CallSuper
@@ -35,9 +22,8 @@ abstract class ImmichPlugin : FlutterPlugin {
fun <T> completeWhenActive(callback: (T) -> Unit, value: T) {
if (detached) {
return
return;
}
callback(value)
callback(value);
}
}

View File

@@ -1,354 +0,0 @@
// 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

@@ -1,239 +0,0 @@
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

@@ -0,0 +1,133 @@
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,6 +553,8 @@ 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 {
@@ -763,6 +765,42 @@ 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,4 +29,9 @@ 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,4 +99,29 @@ 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,6 +4,7 @@ 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
@@ -16,6 +17,8 @@ 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
@@ -38,11 +41,12 @@ sealed class AssetResult {
private const val TAG = "NativeSyncApiImplBase"
@SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
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
@@ -373,11 +377,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
)?.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,16 +477,33 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
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> {

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="0" android:startY="0" android:endX="0" android:endY="108">
<item android:offset="0" android:color="#FFFFF1F2" />
<item android:offset="1" android:color="#FFFECDD3" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="0" android:startY="0" android:endX="108" android:endY="108">
<item android:offset="0" android:color="#FF2B2118" />
<item android:offset="1" android:color="#FF141009" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="0" android:startY="0" android:endX="108" android:endY="108">
<item android:offset="0" android:color="#FF1B2A4A" />
<item android:offset="1" android:color="#FF0A0F1E" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient android:type="radial" android:centerX="54" android:centerY="54" android:gradientRadius="77">
<item android:offset="0" android:color="#FF1A1033" />
<item android:offset="1" android:color="#FF050208" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="0" android:startY="0" android:endX="0" android:endY="108">
<item android:offset="0" android:color="#FF3B1D6E" />
<item android:offset="1" android:color="#FF150A38" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg_blossom"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_blossom"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background_forest"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_forest"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg_gold"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_gold"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background_ink"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_ink"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg_midnight"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_midnight"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg_neon"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_neon"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background_ocean"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_ocean"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background_retro"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_retro"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background_scenic"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_scenic"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg_sunset"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground_sunset"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background_retro">#FFFFFF</color>
<color name="ic_launcher_background_scenic">#FFFFFF</color>
<color name="ic_launcher_background_ocean">#FFFFFF</color>
<color name="ic_launcher_background_forest">#FFFFFF</color>
<color name="ic_launcher_background_ink">#FFFFFF</color>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

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