mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 23:52:32 -04:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0281de7ff6 | |||
| 43554fc6cf | |||
| fd7ddfef54 | |||
| 0975b1599c | |||
| 78ac0ade01 | |||
| 7b9dab872b | |||
| 6413495fb8 | |||
| b414b3d32b | |||
| 20da7c4267 | |||
| 92b6778d2d | |||
| 5a61e589e8 | |||
| 85192bb110 | |||
| c7ae97fa2b | |||
| 8d02f3625d | |||
| a5a7380a26 | |||
| d9ce3d2046 | |||
| 815ff677fc | |||
| 915d865ce2 | |||
| c28e5f90b6 | |||
| 4383473ed6 | |||
| 77701dd5a3 | |||
| d4808fdc4d | |||
| 7fa967a98e | |||
| 9cffcc9f4e | |||
| 40925f0a06 | |||
| 0544d22902 | |||
| 3d075f2bf8 | |||
| 7384799f19 | |||
| 4a7f06e8fd | |||
| 8f662fc459 | |||
| 24b1dae9f2 | |||
| 3a3469a5f9 | |||
| 7993619ed2 | |||
| 4d1f6f869b | |||
| 3eb03f7934 | |||
| 03ed3daa31 | |||
| 02581e81a7 | |||
| 3ab3d5cf43 | |||
| 0ef04d9baa | |||
| df016f9228 | |||
| 17779c1e74 | |||
| 01d6a244d8 | |||
| 7015e511e8 | |||
| 96420bbf04 | |||
| f4e275a257 | |||
| 561fe231ac | |||
| 6b291c469e | |||
| 7d5be4317f | |||
| eee3d2ce61 | |||
| e2f5308cba | |||
| d96cb8d386 | |||
| 2c9639f18b | |||
| 880155916f | |||
| 84854a8575 | |||
| fde0959579 | |||
| ca203726dc | |||
| 5d33870403 | |||
| 0276e86895 | |||
| 90d9d0075a | |||
| 6b7b029562 | |||
| 7adc568575 | |||
| f5dd2cfb18 | |||
| 8c143d36ef | |||
| 45411f38e8 | |||
| 28dda8e2d5 | |||
| dc15af4e69 | |||
| 2775a09dc5 | |||
| 80c9796abe | |||
| 66a3aa27b5 | |||
| 275c324e8d | |||
| 4354431327 | |||
| 0d4d59c7e7 | |||
| b3b0b0f576 | |||
| 4806dc76aa | |||
| 719c7d955b | |||
| 175f8d99de | |||
| fb66f53410 | |||
| 136379a882 | |||
| c35c948f63 | |||
| bc301a3aac | |||
| 3ab68a4bf8 | |||
| 66c6daeded | |||
| bb803f13da | |||
| bda0ceb2e2 | |||
| ef80a8e936 |
@@ -16,7 +16,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- ../packages/plugins:/build/corePlugin
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -159,14 +159,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Comment APK download link on PR
|
- name: Comment APK download link on PR
|
||||||
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
|
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
|
||||||
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
env:
|
env:
|
||||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
|
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
id: mobile-android-apk
|
||||||
message-id: 'mobile-android-apk'
|
token: ${{ steps.token.outputs.token }}
|
||||||
message: |
|
body: |
|
||||||
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
|
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
|
||||||
|
|
||||||
Download: ${{ env.APK_URL }}
|
Download: ${{ env.APK_URL }}
|
||||||
@@ -216,7 +216,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
run: mise //mobile:codegen:pigeon
|
run: mise //mobile:codegen:pigeon
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -288,7 +288,6 @@ jobs:
|
|||||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||||
ENVIRONMENT: ${{ inputs.environment || 'development' }}
|
ENVIRONMENT: ${{ inputs.environment || 'development' }}
|
||||||
BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
|
|
||||||
GITHUB_REF: ${{ github.ref }}
|
GITHUB_REF: ${{ github.ref }}
|
||||||
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
|
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
|
||||||
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
|
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
|
uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -213,12 +213,11 @@ jobs:
|
|||||||
run: 'mise run //deployment:tf apply'
|
run: 'mise run //deployment:tf apply'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
||||||
with:
|
with:
|
||||||
|
id: docs-pr-url
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
||||||
body: |
|
body: |
|
||||||
📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }})
|
📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }})
|
||||||
emojis: 'rocket'
|
|
||||||
body-include: '<!-- Docs PR URL -->'
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -44,9 +44,8 @@ jobs:
|
|||||||
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
with:
|
with:
|
||||||
|
id: docs-pr-url
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
number: ${{ github.event.number }}
|
|
||||||
delete: true
|
delete: true
|
||||||
body-include: '<!-- Docs PR URL -->'
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
if: ${{ inputs.skip != true }}
|
if: ${{ inputs.skip != true }}
|
||||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ jobs:
|
|||||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
id: preview-status
|
||||||
message-id: 'preview-status'
|
token: ${{ steps.token.outputs.token }}
|
||||||
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
|
body: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
|
||||||
|
|
||||||
remove-label:
|
remove-label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -48,16 +48,16 @@ jobs:
|
|||||||
name: 'preview'
|
name: 'preview'
|
||||||
})
|
})
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
id: preview-status
|
||||||
message-id: 'preview-status'
|
token: ${{ steps.token.outputs.token }}
|
||||||
message: 'PRs from forks cannot have preview environments.'
|
body: 'PRs from forks cannot have preview environments.'
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
id: preview-status
|
||||||
message-id: 'preview-status'
|
token: ${{ steps.token.outputs.token }}
|
||||||
message: 'Preview environment has been removed.'
|
body: 'Preview environment has been removed.'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
+30
-27
@@ -30,25 +30,32 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
i18n:
|
i18n:
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
|
- 'mise.toml'
|
||||||
web:
|
web:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
- 'packages/sdk/**'
|
- 'packages/sdk/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'mise.toml'
|
||||||
server:
|
server:
|
||||||
- 'server/**'
|
- 'server/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'mise.toml'
|
||||||
cli:
|
cli:
|
||||||
- 'packages/cli/**'
|
- 'packages/cli/**'
|
||||||
- 'packages/sdk/**'
|
- 'packages/sdk/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'mise.toml'
|
||||||
e2e:
|
e2e:
|
||||||
- 'e2e/**'
|
- 'e2e/**'
|
||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'mise.toml'
|
||||||
mobile:
|
mobile:
|
||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
|
- 'mise.toml'
|
||||||
machine-learning:
|
machine-learning:
|
||||||
- 'machine-learning/**'
|
- 'machine-learning/**'
|
||||||
|
- 'mise.toml'
|
||||||
.github:
|
.github:
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
force-filters: |
|
force-filters: |
|
||||||
@@ -62,9 +69,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -79,12 +83,12 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Run ci-unit
|
- name: Run ci-unit
|
||||||
run: mise run ci-unit
|
run: mise run //server:ci-unit
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Unit Test CLI
|
name: Unit Test CLI
|
||||||
@@ -110,7 +114,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -141,7 +145,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -185,7 +189,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -251,7 +255,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -301,7 +305,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -334,7 +338,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -380,7 +384,7 @@ jobs:
|
|||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup packages
|
- name: Setup packages
|
||||||
run: pnpm --filter "@immich/*" install --frozen-lockfile && pnpm --filter "@immich/*" build
|
run: pnpm --filter @immich/sdk --filter @immich/cli install --frozen-lockfile && pnpm --filter @immich/sdk --filter @immich/cli build
|
||||||
|
|
||||||
- name: Run setup web
|
- name: Run setup web
|
||||||
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
||||||
@@ -553,7 +557,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -590,7 +594,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -621,7 +625,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
@@ -672,13 +676,12 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: mise //:open-api
|
run: mise //:open-api
|
||||||
working-directory: open-api
|
working-directory: open-api
|
||||||
@@ -717,9 +720,6 @@ jobs:
|
|||||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
@@ -734,25 +734,28 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build plugins
|
||||||
|
run: mise //:plugins
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: mise //server:build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: pnpm migrations:run
|
run: pnpm --filter immich migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: pnpm schema:reset
|
run: pnpm --filter immich schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: pnpm migrations:generate src/TestMigration
|
run: pnpm --filter migrations:generate src/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -768,7 +771,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated migration files not up to date!"
|
echo "ERROR: Generated migration files not up to date!"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
cat ./src/*-TestMigration.ts
|
cat ./server/src/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools.opentofu]]
|
||||||
|
version = "1.11.6"
|
||||||
|
backend = "aqua:opentofu/opentofu"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.terragrunt]]
|
||||||
|
version = "1.0.3"
|
||||||
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||||
@@ -74,7 +74,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos:/data
|
- ${UPLOAD_LOCATION}/photos:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- ../packages/plugins:/build/corePlugin
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ make e2e
|
|||||||
Before you can run the tests, you need to run the following commands _once_:
|
Before you can run the tests, you need to run the following commands _once_:
|
||||||
|
|
||||||
- `pnpm install`
|
- `pnpm install`
|
||||||
- `pnpm --filter "@immich/*" build`
|
- `pnpm --filter @immich/sdk --filter @immich/cli build`
|
||||||
- `mise //:open-api`
|
- `mise //:open-api`
|
||||||
|
|
||||||
Once the test environment is running, the e2e tests can be run via:
|
Once the test environment is running, the e2e tests can be run via:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const config = {
|
|||||||
url: 'https://docs.immich.app',
|
url: 'https://docs.immich.app',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
favicon: 'img/favicon.png',
|
favicon: 'img/favicon.png',
|
||||||
|
|
||||||
// GitHub pages deployment config.
|
// GitHub pages deployment config.
|
||||||
@@ -29,6 +28,9 @@ const config = {
|
|||||||
// Mermaid diagrams
|
// Mermaid diagrams
|
||||||
markdown: {
|
markdown: {
|
||||||
mermaid: true,
|
mermaid: true,
|
||||||
|
hooks: {
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
themes: ['@docusaurus/theme-mermaid'],
|
themes: ['@docusaurus/theme-mermaid'],
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools.wrangler]]
|
||||||
|
version = "4.66.0"
|
||||||
|
backend = "npm:wrangler"
|
||||||
+1
-1
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
|||||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
wrangler = "4.66.0"
|
wrangler = "4.91.0"
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.4",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^7.0.0",
|
"@types/supertest": "^7.0.0",
|
||||||
|
|||||||
+26
-8
@@ -22,13 +22,12 @@
|
|||||||
"add_birthday": "Add a birthday",
|
"add_birthday": "Add a birthday",
|
||||||
"add_endpoint": "Add endpoint",
|
"add_endpoint": "Add endpoint",
|
||||||
"add_exclusion_pattern": "Add exclusion pattern",
|
"add_exclusion_pattern": "Add exclusion pattern",
|
||||||
"add_filter": "Add filter",
|
|
||||||
"add_filter_description": "Click to add a filter condition",
|
|
||||||
"add_location": "Add location",
|
"add_location": "Add location",
|
||||||
"add_more_users": "Add more users",
|
"add_more_users": "Add more users",
|
||||||
"add_partner": "Add partner",
|
"add_partner": "Add partner",
|
||||||
"add_path": "Add path",
|
"add_path": "Add path",
|
||||||
"add_photos": "Add photos",
|
"add_photos": "Add photos",
|
||||||
|
"add_step": "Add step",
|
||||||
"add_tag": "Add tag",
|
"add_tag": "Add tag",
|
||||||
"add_to": "Add to…",
|
"add_to": "Add to…",
|
||||||
"add_to_album": "Add to album",
|
"add_to_album": "Add to album",
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
"add_to_shared_album": "Add to shared album",
|
"add_to_shared_album": "Add to shared album",
|
||||||
"add_upload_to_stack": "Add upload to stack",
|
"add_upload_to_stack": "Add upload to stack",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
"add_workflow_step": "Add workflow step",
|
|
||||||
"added_to_archive": "Added to archive",
|
"added_to_archive": "Added to archive",
|
||||||
"added_to_favorites": "Added to favorites",
|
"added_to_favorites": "Added to favorites",
|
||||||
"added_to_favorites_count": "Added {count, number} to favorites",
|
"added_to_favorites_count": "Added {count, number} to favorites",
|
||||||
@@ -733,6 +731,7 @@
|
|||||||
"cannot_update_the_description": "Cannot update the description",
|
"cannot_update_the_description": "Cannot update the description",
|
||||||
"cast": "Cast",
|
"cast": "Cast",
|
||||||
"cast_description": "Configure available cast destinations",
|
"cast_description": "Configure available cast destinations",
|
||||||
|
"change": "Change",
|
||||||
"change_date": "Change date",
|
"change_date": "Change date",
|
||||||
"change_description": "Change description",
|
"change_description": "Change description",
|
||||||
"change_display_order": "Change display order",
|
"change_display_order": "Change display order",
|
||||||
@@ -761,6 +760,7 @@
|
|||||||
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||||
"check_logs": "Check Logs",
|
"check_logs": "Check Logs",
|
||||||
"checksum": "Checksum",
|
"checksum": "Checksum",
|
||||||
|
"choose": "Choose",
|
||||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
"cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?",
|
"cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?",
|
||||||
@@ -778,6 +778,7 @@
|
|||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"clear_all_recent_searches": "Clear all recent searches",
|
"clear_all_recent_searches": "Clear all recent searches",
|
||||||
|
"clear_failed_count": "Clear failed ({count})",
|
||||||
"clear_file_cache": "Clear File Cache",
|
"clear_file_cache": "Clear File Cache",
|
||||||
"clear_message": "Clear message",
|
"clear_message": "Clear message",
|
||||||
"clear_value": "Clear value",
|
"clear_value": "Clear value",
|
||||||
@@ -809,6 +810,7 @@
|
|||||||
"comments_are_disabled": "Comments are disabled",
|
"comments_are_disabled": "Comments are disabled",
|
||||||
"common_create_new_album": "Create new album",
|
"common_create_new_album": "Create new album",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
"configuration": "Configuration",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"confirm_admin_password": "Confirm Admin Password",
|
"confirm_admin_password": "Confirm Admin Password",
|
||||||
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
||||||
@@ -823,6 +825,7 @@
|
|||||||
"contain": "Contain",
|
"contain": "Contain",
|
||||||
"context": "Context",
|
"context": "Context",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
|
"control_bottom_app_bar_add_tags": "Add Tags",
|
||||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||||
@@ -894,6 +897,7 @@
|
|||||||
"date_of_birth": "Date of birth",
|
"date_of_birth": "Date of birth",
|
||||||
"date_of_birth_saved": "Date of birth saved successfully",
|
"date_of_birth_saved": "Date of birth saved successfully",
|
||||||
"date_range": "Date range",
|
"date_range": "Date range",
|
||||||
|
"date_time_original": "Date/Time Original",
|
||||||
"day": "Day",
|
"day": "Day",
|
||||||
"days": "Days",
|
"days": "Days",
|
||||||
"deduplicate_all": "Deduplicate All",
|
"deduplicate_all": "Deduplicate All",
|
||||||
@@ -1074,6 +1078,7 @@
|
|||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"failed_to_remove_product_key": "Failed to remove product key",
|
||||||
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
||||||
"failed_to_stack_assets": "Failed to stack assets",
|
"failed_to_stack_assets": "Failed to stack assets",
|
||||||
|
"failed_to_tag_assets": "Failed to tag assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
"failed_to_update_notification_status": "Failed to update notification status",
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
"incorrect_email_or_password": "Incorrect email or password",
|
"incorrect_email_or_password": "Incorrect email or password",
|
||||||
@@ -1193,11 +1198,13 @@
|
|||||||
"export_as_json": "Export as JSON",
|
"export_as_json": "Export as JSON",
|
||||||
"export_database": "Export Database",
|
"export_database": "Export Database",
|
||||||
"export_database_description": "Export the SQLite database",
|
"export_database_description": "Export the SQLite database",
|
||||||
|
"exposure_time": "Exposure Time",
|
||||||
"extension": "Extension",
|
"extension": "Extension",
|
||||||
"external": "External",
|
"external": "External",
|
||||||
"external_libraries": "External Libraries",
|
"external_libraries": "External Libraries",
|
||||||
"external_network": "External network",
|
"external_network": "External network",
|
||||||
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||||
|
"f_number": "F-Number",
|
||||||
"face_unassigned": "Unassigned",
|
"face_unassigned": "Unassigned",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"failed_count": "Failed: {count}",
|
"failed_count": "Failed: {count}",
|
||||||
@@ -1215,7 +1222,6 @@
|
|||||||
"features_setting_description": "Manage the app features",
|
"features_setting_description": "Manage the app features",
|
||||||
"file_name_or_extension": "File name or extension",
|
"file_name_or_extension": "File name or extension",
|
||||||
"file_name_text": "File name",
|
"file_name_text": "File name",
|
||||||
"file_name_with_value": "File name: {file_name}",
|
|
||||||
"file_size": "File size",
|
"file_size": "File size",
|
||||||
"filename": "Filename",
|
"filename": "Filename",
|
||||||
"filetype": "Filetype",
|
"filetype": "Filetype",
|
||||||
@@ -1228,6 +1234,7 @@
|
|||||||
"find_them_fast": "Find them fast by name with search",
|
"find_them_fast": "Find them fast by name with search",
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"fix_incorrect_match": "Fix incorrect match",
|
"fix_incorrect_match": "Fix incorrect match",
|
||||||
|
"focal_length": "Focal Length",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"folder_not_found": "Folder not found",
|
"folder_not_found": "Folder not found",
|
||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
@@ -1348,6 +1355,7 @@
|
|||||||
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
|
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
|
||||||
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
|
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
|
||||||
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
|
||||||
|
"iso": "ISO",
|
||||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"json_editor": "JSON editor",
|
"json_editor": "JSON editor",
|
||||||
@@ -1580,6 +1588,7 @@
|
|||||||
"mobile_app": "Mobile App",
|
"mobile_app": "Mobile App",
|
||||||
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
|
"modify_date": "Modify Date",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"motion": "Motion",
|
"motion": "Motion",
|
||||||
@@ -1628,7 +1637,6 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"next_memory": "Next memory",
|
"next_memory": "Next memory",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"no_actions_added": "No actions added yet",
|
|
||||||
"no_albums_found": "No albums found",
|
"no_albums_found": "No albums found",
|
||||||
"no_albums_message": "Create an album to organize your photos and videos",
|
"no_albums_message": "Create an album to organize your photos and videos",
|
||||||
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
||||||
@@ -1645,7 +1653,6 @@
|
|||||||
"no_exif_info_available": "No exif info available",
|
"no_exif_info_available": "No exif info available",
|
||||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
"no_explore_results_message": "Upload more photos to explore your collection.",
|
||||||
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
||||||
"no_filters_added": "No filters added yet",
|
|
||||||
"no_libraries_message": "Create an external library to view your photos and videos",
|
"no_libraries_message": "Create an external library to view your photos and videos",
|
||||||
"no_local_assets_found": "No local assets found with this checksum",
|
"no_local_assets_found": "No local assets found with this checksum",
|
||||||
"no_location_set": "No location set",
|
"no_location_set": "No location set",
|
||||||
@@ -1658,6 +1665,7 @@
|
|||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
"no_results_description": "Try a synonym or more general keyword",
|
"no_results_description": "Try a synonym or more general keyword",
|
||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
|
"no_steps": "No steps added yet",
|
||||||
"no_uploads_in_progress": "No uploads in progress",
|
"no_uploads_in_progress": "No uploads in progress",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"not_allowed": "Not allowed",
|
"not_allowed": "Not allowed",
|
||||||
@@ -1703,6 +1711,7 @@
|
|||||||
"organize_into_albums": "Organize into albums",
|
"organize_into_albums": "Organize into albums",
|
||||||
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
||||||
"organize_your_library": "Organize your library",
|
"organize_your_library": "Organize your library",
|
||||||
|
"orientation": "Orientation",
|
||||||
"original": "original",
|
"original": "original",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
"other_devices": "Other devices",
|
"other_devices": "Other devices",
|
||||||
@@ -1794,6 +1803,8 @@
|
|||||||
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
|
||||||
"play_transcoded_video": "Play transcoded video",
|
"play_transcoded_video": "Play transcoded video",
|
||||||
"please_auth_to_access": "Please authenticate to access",
|
"please_auth_to_access": "Please authenticate to access",
|
||||||
|
"plugin_method_filter_type": "Filter",
|
||||||
|
"plugin_method_filter_type_description": "This method can filter events and conditionally prevent subsequent steps from running",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
"preferences_settings_title": "Preferences",
|
"preferences_settings_title": "Preferences",
|
||||||
@@ -1815,6 +1826,7 @@
|
|||||||
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
||||||
"profile_image_of_user": "Profile image of {user}",
|
"profile_image_of_user": "Profile image of {user}",
|
||||||
"profile_picture_set": "Profile picture set.",
|
"profile_picture_set": "Profile picture set.",
|
||||||
|
"projection_type": "Projection Type",
|
||||||
"public_album": "Public album",
|
"public_album": "Public album",
|
||||||
"public_share": "Public Share",
|
"public_share": "Public Share",
|
||||||
"purchase_account_info": "Supporter",
|
"purchase_account_info": "Supporter",
|
||||||
@@ -2184,7 +2196,9 @@
|
|||||||
"show_in_timeline": "Show in timeline",
|
"show_in_timeline": "Show in timeline",
|
||||||
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
|
||||||
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
"show_keyboard_shortcuts": "Show keyboard shortcuts",
|
||||||
|
"show_less": "Show less",
|
||||||
"show_metadata": "Show metadata",
|
"show_metadata": "Show metadata",
|
||||||
|
"show_more_fields": "{count, plural, one {Show # more field} other {Show # more fields}}",
|
||||||
"show_or_hide_info": "Show or hide info",
|
"show_or_hide_info": "Show or hide info",
|
||||||
"show_password": "Show password",
|
"show_password": "Show password",
|
||||||
"show_person_options": "Show person options",
|
"show_person_options": "Show person options",
|
||||||
@@ -2236,6 +2250,10 @@
|
|||||||
"start_date_before_end_date": "Start date must be before end date",
|
"start_date_before_end_date": "Start date must be before end date",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
"step_delete": "Delete step",
|
||||||
|
"step_delete_confirm": "Are you sure you want to delete this step?",
|
||||||
|
"step_details": "Step details",
|
||||||
|
"steps": "Steps",
|
||||||
"stop_casting": "Stop casting",
|
"stop_casting": "Stop casting",
|
||||||
"stop_motion_photo": "Stop Motion Photo",
|
"stop_motion_photo": "Stop Motion Photo",
|
||||||
"stop_photo_sharing": "Stop sharing your photos?",
|
"stop_photo_sharing": "Stop sharing your photos?",
|
||||||
@@ -2329,7 +2347,7 @@
|
|||||||
"trash_page_title": "Trash ({count})",
|
"trash_page_title": "Trash ({count})",
|
||||||
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"trigger_asset_uploaded": "Asset Uploaded",
|
"trigger_asset_uploaded": "Asset Upload",
|
||||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||||
"trigger_description": "An event that kicks off the workflow",
|
"trigger_description": "An event that kicks off the workflow",
|
||||||
"trigger_person_recognized": "Person Recognized",
|
"trigger_person_recognized": "Person Recognized",
|
||||||
@@ -2369,7 +2387,6 @@
|
|||||||
"unsupported_field_type": "Unsupported field type",
|
"unsupported_field_type": "Unsupported field type",
|
||||||
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
"untitled_workflow": "Untitled workflow",
|
|
||||||
"up_next": "Up next",
|
"up_next": "Up next",
|
||||||
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
||||||
"updated_at": "Updated",
|
"updated_at": "Updated",
|
||||||
@@ -2461,6 +2478,7 @@
|
|||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
"width": "Width",
|
"width": "Width",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
|
"workflow": "Workflow",
|
||||||
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
|
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
|
||||||
"workflow_deleted": "Workflow deleted",
|
"workflow_deleted": "Workflow deleted",
|
||||||
"workflow_description": "Workflow description",
|
"workflow_description": "Workflow description",
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools.python]]
|
||||||
|
version = "3.11.15"
|
||||||
|
backend = "core:python"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:243f794278eff6adba96ed3677ec6877175df84c25f140e17f09f9be82d0f12a"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:52b4c52094ff8b383a45c694acf4c5c0e883152be6d5229a35a8186ce907c6eb"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:171dffd8c0f66e8a0725364a7428015b22fc18dd298b24f541392e17dd0e561f"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:2ac90fef8917ebd14826a6d667593a06cf0ae5f745ba9b1147dc086dd35f5284"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:fdfc363b538662eb7441a14e06f72c4a992c56af7f401f5730ea5081f8f8ad6e"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:5f1eb247cbca2c0ad5ccbf6d299a4f54b31b5c63b492d74c3531dc4344a42f88"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:756d7f148498b8822f6aedf44a020613576f09983161f346ad36dcef6238cdc3"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[[tools.uv]]
|
||||||
|
version = "0.8.15"
|
||||||
|
backend = "aqua:astral-sh/uv"
|
||||||
|
|
||||||
|
[tools.uv."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
|
||||||
|
|
||||||
|
[tools.uv."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
|
||||||
|
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools."aqua:flutter/flutter"]]
|
||||||
|
version = "3.41.9"
|
||||||
|
backend = "aqua:flutter/flutter"
|
||||||
|
|
||||||
|
[[tools.flutter]]
|
||||||
|
version = "3.41.9-stable"
|
||||||
|
backend = "asdf:flutter"
|
||||||
|
|
||||||
|
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||||
|
version = "1.37.0"
|
||||||
|
backend = "github:CQLabs/homebrew-dcm"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||||
|
|
||||||
|
[[tools."github:extism/cli"]]
|
||||||
|
version = "1.6.3"
|
||||||
|
backend = "github:extism/cli"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694029"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:9a2f71b6e6009685a622cc3084e52d2a1a8e23c98d29ffa72e666e9dc699855f"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-amd64.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694026"
|
||||||
|
|
||||||
|
[tools."github:extism/cli"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:47e4ed2782445b2b08a4d1ac127211588f8b4d1fc25fd6481d4cb65151b5213c"
|
||||||
|
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-windows-amd64.zip"
|
||||||
|
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
|
||||||
|
|
||||||
|
[[tools."github:extism/js-pdk"]]
|
||||||
|
version = "1.6.0"
|
||||||
|
backend = "github:extism/js-pdk"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239"
|
||||||
|
|
||||||
|
[tools."github:extism/js-pdk"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc"
|
||||||
|
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz"
|
||||||
|
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
||||||
|
|
||||||
|
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||||
|
version = "7.1.3-6"
|
||||||
|
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||||
|
|
||||||
|
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
||||||
|
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||||
|
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||||
|
|
||||||
|
[[tools."github:webassembly/binaryen"]]
|
||||||
|
version = "version_124"
|
||||||
|
backend = "github:webassembly/binaryen"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926134"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:b389bb0731758d86c3cb266d01d28a12725c23bd3cabc3df34faa162af0887e9"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-macos.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926135"
|
||||||
|
|
||||||
|
[tools."github:webassembly/binaryen"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2203"
|
||||||
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
|
||||||
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
|
||||||
|
|
||||||
|
[[tools.java]]
|
||||||
|
version = "21.0.2"
|
||||||
|
backend = "core:java"
|
||||||
|
|
||||||
|
[tools.java."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||||
|
|
||||||
|
[[tools.node]]
|
||||||
|
version = "24.15.0"
|
||||||
|
backend = "core:node"
|
||||||
|
|
||||||
|
[tools.node."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed"
|
||||||
|
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:31e98aa960a067da91edffd5d93bc46657b5d2a8029612c359f5f2ac0060152a"
|
||||||
|
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89"
|
||||||
|
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:f55af5bd489c5347b113ca6594cae00a54b30ba57ac5875324311bfc6f4762e3"
|
||||||
|
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
|
||||||
|
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b"
|
||||||
|
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.gz"
|
||||||
|
|
||||||
|
[tools.node."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
|
||||||
|
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"
|
||||||
|
|
||||||
|
[[tools."npm:oazapfts"]]
|
||||||
|
version = "7.5.0"
|
||||||
|
backend = "npm:oazapfts"
|
||||||
|
|
||||||
|
[[tools.opentofu]]
|
||||||
|
version = "1.11.6"
|
||||||
|
backend = "aqua:opentofu/opentofu"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.opentofu."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||||
|
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||||
|
|
||||||
|
[[tools.pnpm]]
|
||||||
|
version = "10.33.4"
|
||||||
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
|
||||||
|
[[tools.terragrunt]]
|
||||||
|
version = "1.0.3"
|
||||||
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||||
|
|
||||||
|
[tools.terragrunt."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||||
|
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||||
@@ -2,7 +2,7 @@ experimental_monorepo_root = true
|
|||||||
|
|
||||||
[monorepo]
|
[monorepo]
|
||||||
config_roots = [
|
config_roots = [
|
||||||
"packages/plugins",
|
"packages/plugin-core",
|
||||||
"server",
|
"server",
|
||||||
"packages/cli",
|
"packages/cli",
|
||||||
"deployment",
|
"deployment",
|
||||||
@@ -16,18 +16,28 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
flutter = "3.41.9"
|
"aqua:flutter/flutter" = "3.41.9"
|
||||||
pnpm = "10.33.1"
|
pnpm = "10.33.4"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
"npm:oazapfts" = "7.5.0"
|
"npm:oazapfts" = "7.5.0"
|
||||||
|
"github:extism/cli" = "1.6.3"
|
||||||
|
"github:webassembly/binaryen" = "version_124"
|
||||||
|
"github:extism/js-pdk" = "1.6.0"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
bin = "dcm"
|
bin = "dcm"
|
||||||
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm".platforms]
|
||||||
|
linux-x64 = { asset_pattern = "dcm-linux-x64-release.zip" }
|
||||||
|
linux-arm64 = { asset_pattern = "dcm-linux-arm-release.zip" }
|
||||||
|
macos-x64 = { asset_pattern = "dcm-macos-x64-release.zip" }
|
||||||
|
macos-arm64 = { asset_pattern = "dcm-macos-arm-release.zip" }
|
||||||
|
windows-x64 = { asset_pattern = "dcm-windows-release.zip" }
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
|
|
||||||
@@ -40,6 +50,13 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
|
|||||||
[settings]
|
[settings]
|
||||||
experimental = true
|
experimental = true
|
||||||
pin = true
|
pin = true
|
||||||
|
lockfile = true
|
||||||
|
|
||||||
|
[tasks.plugins]
|
||||||
|
run = [
|
||||||
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||||
|
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
||||||
|
]
|
||||||
|
|
||||||
[tasks.open-api-typescript]
|
[tasks.open-api-typescript]
|
||||||
run = [
|
run = [
|
||||||
@@ -55,11 +72,13 @@ run = "bash ./bin/generate-dart-sdk.sh"
|
|||||||
[tasks.open-api]
|
[tasks.open-api]
|
||||||
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
||||||
run = [
|
run = [
|
||||||
|
{ task = "//:plugins" },
|
||||||
|
{ task = "//server:build" },
|
||||||
{ task = "//server:install" },
|
{ task = "//server:install" },
|
||||||
{ task = "//server:build" },
|
{ task = "//server:build" },
|
||||||
{ task = "//server:sync-open-api" },
|
{ task = "//server:sync-open-api" },
|
||||||
{ task = ":open-api-typescript"},
|
{ task = ":open-api-typescript" },
|
||||||
{ task = ":open-api-dart"},
|
{ task = ":open-api-dart" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks.sql]
|
[tasks.sql]
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.okhttp
|
||||||
implementation libs.cronet.embedded
|
implementation libs.cronet.embedded
|
||||||
implementation libs.media3.datasource.okhttp
|
implementation libs.media3.datasource.okhttp
|
||||||
|
|||||||
@@ -89,6 +89,20 @@
|
|||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Allow Immich to act as an image viewer -->
|
||||||
|
<intent-filter android:label="View in Immich">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:scheme="content" android:mimeType="image/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Allow Immich to act as a video viewer -->
|
||||||
|
<intent-filter android:label="View in Immich">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:scheme="content" android:mimeType="video/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- immich:// URL scheme handling -->
|
<!-- immich:// URL scheme handling -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import app.alextran.immich.background.BackgroundEngineLock
|
import app.alextran.immich.background.BackgroundEngineLock
|
||||||
@@ -17,9 +18,12 @@ import app.alextran.immich.images.LocalImageApi
|
|||||||
import app.alextran.immich.images.LocalImagesImpl
|
import app.alextran.immich.images.LocalImagesImpl
|
||||||
import app.alextran.immich.images.RemoteImageApi
|
import app.alextran.immich.images.RemoteImageApi
|
||||||
import app.alextran.immich.images.RemoteImagesImpl
|
import app.alextran.immich.images.RemoteImagesImpl
|
||||||
|
import app.alextran.immich.permission.PermissionApi
|
||||||
|
import app.alextran.immich.permission.PermissionApiImpl
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl26
|
import app.alextran.immich.sync.NativeSyncApiImpl26
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||||
|
import app.alextran.immich.viewintent.ViewIntentPlugin
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
@@ -29,6 +33,11 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
registerPlugins(this, flutterEngine)
|
registerPlugins(this, flutterEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
||||||
HttpClientManager.initialize(ctx)
|
HttpClientManager.initialize(ctx)
|
||||||
@@ -44,15 +53,19 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
NativeSyncApiImpl30(ctx)
|
NativeSyncApiImpl30(ctx)
|
||||||
}
|
}
|
||||||
|
val permissionApiImpl = PermissionApiImpl(ctx)
|
||||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||||
|
PermissionApi.setUp(messenger, permissionApiImpl)
|
||||||
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
||||||
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
||||||
|
|
||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|
||||||
|
flutterEngine.plugins.add(ViewIntentPlugin())
|
||||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||||
|
flutterEngine.plugins.add(permissionApiImpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
||||||
@@ -60,6 +73,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
|
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
|
||||||
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
|
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
|
||||||
nativeApi?.detachFromEngine()
|
nativeApi?.detachFromEngine()
|
||||||
|
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
|
||||||
|
permissionApi?.detachFromEngine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ interface NetworkApi {
|
|||||||
fun hasCertificate(): Boolean
|
fun hasCertificate(): Boolean
|
||||||
fun getClientPointer(): Long
|
fun getClientPointer(): Long
|
||||||
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
|
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
|
||||||
|
fun getAppGroupId(): String
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by NetworkApi. */
|
/** The codec used by NetworkApi. */
|
||||||
@@ -430,6 +431,21 @@ interface NetworkApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getAppGroupId())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
NetworkPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
|
|||||||
private var networkApi: NetworkApiImpl? = null
|
private var networkApi: NetworkApiImpl? = null
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
networkApi = NetworkApiImpl()
|
networkApi = NetworkApiImpl(binding.applicationContext)
|
||||||
NetworkApi.setUp(binding.binaryMessenger, networkApi)
|
NetworkApi.setUp(binding.binaryMessenger, networkApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +39,11 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NetworkApiImpl : NetworkApi {
|
private class NetworkApiImpl(private val context: Context) : NetworkApi {
|
||||||
var activity: Activity? = null
|
var activity: Activity? = null
|
||||||
|
|
||||||
|
override fun getAppGroupId(): String = context.packageName
|
||||||
|
|
||||||
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
|
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
|
||||||
try {
|
try {
|
||||||
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
|
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import java.io.IOException
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
|
||||||
|
|
||||||
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||||
|
|
||||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
@@ -228,7 +230,6 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
private val onComplete: () -> Unit,
|
private val onComplete: () -> Unit,
|
||||||
) : UrlRequest.Callback() {
|
) : UrlRequest.Callback() {
|
||||||
private var buffer: NativeByteBuffer? = null
|
private var buffer: NativeByteBuffer? = null
|
||||||
private var wrapped: ByteBuffer? = null
|
|
||||||
private var error: Exception? = null
|
private var error: Exception? = null
|
||||||
|
|
||||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) {
|
||||||
@@ -242,15 +243,16 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Content-Length is a size hint only. With Content-Encoding (gzip/br/...),
|
||||||
|
// Cronet auto-decompresses and writes decompressed bytes to our buffer, which
|
||||||
|
// may exceed the wire/compressed Content-Length. Always use the growable
|
||||||
|
// buffer path so we can't overflow.
|
||||||
val contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0
|
val contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0
|
||||||
if (contentLength > 0) {
|
// Cap the up-front alloc: Content-Length is untrusted and can be huge or near
|
||||||
buffer = NativeByteBuffer(contentLength + 1)
|
// Int.MAX_VALUE (overflowing `+1`). For larger responses the grow path takes over.
|
||||||
wrapped = NativeBuffer.wrap(buffer!!.pointer, contentLength + 1)
|
val initialSize = if (contentLength in 1..MAX_PREALLOC_BYTES) contentLength + 1 else INITIAL_BUFFER_SIZE
|
||||||
request.read(wrapped)
|
buffer = NativeByteBuffer(initialSize)
|
||||||
} else {
|
request.read(buffer!!.wrapRemaining())
|
||||||
buffer = NativeByteBuffer(INITIAL_BUFFER_SIZE)
|
|
||||||
request.read(buffer!!.wrapRemaining())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e
|
error = e
|
||||||
return request.cancel()
|
return request.cancel()
|
||||||
@@ -263,14 +265,14 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
byteBuffer: ByteBuffer
|
byteBuffer: ByteBuffer
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val buf = if (wrapped == null) {
|
// Always pass a fresh wrap so byteBuffer.position() represents only the
|
||||||
buffer!!.run {
|
// bytes Cronet wrote in this iteration. Reusing the caller-supplied
|
||||||
advance(byteBuffer.position())
|
// ByteBuffer breaks advance(): Cronet's position keeps accumulating
|
||||||
ensureHeadroom()
|
// across reads, which would double-count previous iterations' bytes.
|
||||||
wrapRemaining()
|
val buf = buffer!!.run {
|
||||||
}
|
advance(byteBuffer.position())
|
||||||
} else {
|
ensureHeadroom()
|
||||||
wrapped
|
wrapRemaining()
|
||||||
}
|
}
|
||||||
request.read(buf)
|
request.read(buf)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -280,7 +282,6 @@ private class CronetImageFetcher : ImageFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||||
wrapped?.let { buffer!!.advance(it.position()) }
|
|
||||||
onSuccess(buffer!!)
|
onSuccess(buffer!!)
|
||||||
onComplete()
|
onComplete()
|
||||||
}
|
}
|
||||||
|
|||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
|
|
||||||
|
class ManageMediaPermissionDelegate(
|
||||||
|
context: Context,
|
||||||
|
private val requestCode: Int = 1003,
|
||||||
|
) : PluginRegistry.ActivityResultListener {
|
||||||
|
private val ctx = context.applicationContext
|
||||||
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
|
||||||
|
|
||||||
|
fun hasManageMediaPermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
MediaStore.canManageMedia(ctx)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (hasManageMediaPermission()) {
|
||||||
|
callback(Result.success(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openManageMediaPermissionSettings(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
openManageMediaPermissionSettings(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openManageMediaPermissionSettings(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
callback(Result.success(false))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = activityBinding?.activity
|
||||||
|
if (activity == null) {
|
||||||
|
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingResult = callback
|
||||||
|
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA).apply {
|
||||||
|
data = "package:${activity.packageName}".toUri()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingResult = null
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError("ACTIVITY_LAUNCH_FAILED", "Failed to launch MANAGE_MEDIA settings", e.toString())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activityBinding = binding
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetachedFromActivity() {
|
||||||
|
failPending("ACTIVITY_DETACHED", "Activity detached before MANAGE_MEDIA result")
|
||||||
|
activityBinding?.removeActivityResultListener(this)
|
||||||
|
activityBinding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode == this.requestCode) {
|
||||||
|
val callback = pendingResult
|
||||||
|
pendingResult = null
|
||||||
|
callback?.invoke(Result.success(hasManageMediaPermission()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failPending(code: String, message: String) {
|
||||||
|
val callback = pendingResult ?: return
|
||||||
|
pendingResult = null
|
||||||
|
callback(Result.failure(FlutterError(code, message, null)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
// 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.permission
|
||||||
|
|
||||||
|
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 PermissionApiPigeonUtils {
|
||||||
|
|
||||||
|
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 PermissionApiPigeonCodec : 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 PermissionApi {
|
||||||
|
fun hasManageMediaPermission(): Boolean
|
||||||
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by PermissionApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
PermissionApiPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.hasManageMediaPermission())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
PermissionApiPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.requestManageMediaPermission{ result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.manageMediaPermission{ result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.alextran.immich.core.ImmichPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
|
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
||||||
|
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
||||||
|
|
||||||
|
override fun hasManageMediaPermission(): Boolean =
|
||||||
|
manageMediaPermissionDelegate.hasManageMediaPermission()
|
||||||
|
|
||||||
|
override fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
manageMediaPermissionDelegate.requestManageMediaPermission { completeWhenActive(callback, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
manageMediaPermissionDelegate.manageMediaPermission { completeWhenActive(callback, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
manageMediaPermissionDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
manageMediaPermissionDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
manageMediaPermissionDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
manageMediaPermissionDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -553,6 +553,7 @@ interface NativeSyncApi {
|
|||||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||||
fun cancelHashing()
|
fun cancelHashing()
|
||||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||||
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -747,6 +748,27 @@ interface NativeSyncApi {
|
|||||||
channel.setMessageHandler(null)
|
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 {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.bumptech.glide.Glide
|
|||||||
import com.bumptech.glide.load.ImageHeaderParser
|
import com.bumptech.glide.load.ImageHeaderParser
|
||||||
import com.bumptech.glide.load.ImageHeaderParserUtils
|
import com.bumptech.glide.load.ImageHeaderParserUtils
|
||||||
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -39,10 +41,11 @@ sealed class AssetResult {
|
|||||||
private const val TAG = "NativeSyncApiImplBase"
|
private const val TAG = "NativeSyncApiImplBase"
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
private var hashTask: Job? = null
|
private var hashTask: Job? = null
|
||||||
|
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||||
@@ -448,6 +451,26 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
hashTask = null
|
hashTask = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
|
||||||
@Suppress("unused", "UNUSED_PARAMETER")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
||||||
|
|||||||
+292
@@ -0,0 +1,292 @@
|
|||||||
|
// 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.viewintent
|
||||||
|
|
||||||
|
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 ViewIntentPigeonUtils {
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class ViewIntentPayload (
|
||||||
|
val path: String? = null,
|
||||||
|
val mimeType: String,
|
||||||
|
val localAssetId: String? = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): ViewIntentPayload {
|
||||||
|
val path = pigeonVar_list[0] as String?
|
||||||
|
val mimeType = pigeonVar_list[1] as String
|
||||||
|
val localAssetId = pigeonVar_list[2] as String?
|
||||||
|
return ViewIntentPayload(path, mimeType, localAssetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
path,
|
||||||
|
mimeType,
|
||||||
|
localAssetId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || other.javaClass != javaClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val other = other as ViewIntentPayload
|
||||||
|
return ViewIntentPigeonUtils.deepEquals(this.path, other.path) && ViewIntentPigeonUtils.deepEquals(this.mimeType, other.mimeType) && ViewIntentPigeonUtils.deepEquals(this.localAssetId, other.localAssetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = javaClass.hashCode()
|
||||||
|
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.path)
|
||||||
|
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.mimeType)
|
||||||
|
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.localAssetId)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private open class ViewIntentPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
129.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
ViewIntentPayload.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is ViewIntentPayload -> {
|
||||||
|
stream.write(129)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface ViewIntentHostApi {
|
||||||
|
fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by ViewIntentHostApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
ViewIntentPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.consumeViewIntent{ result: Result<ViewIntentPayload?> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(ViewIntentPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(ViewIntentPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
package app.alextran.immich.viewintent
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
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.PluginRegistry
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "ViewIntentPlugin"
|
||||||
|
|
||||||
|
class ViewIntentPlugin : FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener, ViewIntentHostApi {
|
||||||
|
private var context: Context? = null
|
||||||
|
private var activity: Activity? = null
|
||||||
|
private var pendingIntent: Intent? = null
|
||||||
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
context = binding.applicationContext
|
||||||
|
ViewIntentHostApi.setUp(binding.binaryMessenger, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
ViewIntentHostApi.setUp(binding.binaryMessenger, null)
|
||||||
|
ioScope.cancel()
|
||||||
|
context = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activity = binding.activity
|
||||||
|
pendingIntent = binding.activity.intent
|
||||||
|
binding.addOnNewIntentListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent): Boolean {
|
||||||
|
pendingIntent = intent
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit) {
|
||||||
|
val context = context ?: run {
|
||||||
|
callback(Result.success(null))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = pendingIntent ?: activity?.intent
|
||||||
|
|
||||||
|
if (intent?.action != Intent.ACTION_VIEW) {
|
||||||
|
callback(Result.success(null))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = intent.data
|
||||||
|
if (uri == null) {
|
||||||
|
callback(Result.success(null))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ioScope.launch {
|
||||||
|
try {
|
||||||
|
val mimeType = context.contentResolver.getType(uri) ?: intent.type
|
||||||
|
if (mimeType == null || (!mimeType.startsWith("image/") && !mimeType.startsWith("video/"))) {
|
||||||
|
callback(Result.success(null))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val localAssetId = extractLocalAssetId(context, uri, mimeType)
|
||||||
|
val tempFilePath = if (localAssetId == null) {
|
||||||
|
copyUriToTempFile(context, uri, mimeType)?.absolutePath ?: run {
|
||||||
|
callback(Result.success(null))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val payload = ViewIntentPayload(
|
||||||
|
path = tempFilePath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
localAssetId = localAssetId,
|
||||||
|
)
|
||||||
|
consumeViewIntent(intent)
|
||||||
|
callback(Result.success(payload))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun consumeViewIntent(currentIntent: Intent) {
|
||||||
|
pendingIntent = Intent(currentIntent).apply {
|
||||||
|
action = null
|
||||||
|
data = null
|
||||||
|
type = null
|
||||||
|
}
|
||||||
|
activity?.intent = pendingIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractLocalAssetId(context: Context, uri: Uri, mimeType: String): String? {
|
||||||
|
return tryExtractDocumentLocalAssetId(context, uri)
|
||||||
|
?: tryParseContentUriId(uri)
|
||||||
|
?: tryParseLastPathSegmentId(uri)
|
||||||
|
?: resolveLocalIdByNameAndSize(context, uri, mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryExtractDocumentLocalAssetId(context: Context, uri: Uri): String? {
|
||||||
|
return try {
|
||||||
|
if (!DocumentsContract.isDocumentUri(context, uri)) return null
|
||||||
|
val docId = DocumentsContract.getDocumentId(uri)
|
||||||
|
if (docId.isBlank() || docId.startsWith("raw:")) return null
|
||||||
|
val parsed = docId.substringAfter(':', docId)
|
||||||
|
if (parsed.isNotEmpty() && parsed.all(Char::isDigit)) parsed else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to resolve local asset id from document URI: $uri", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryParseContentUriId(uri: Uri): String? {
|
||||||
|
return try {
|
||||||
|
val parsed = ContentUris.parseId(uri)
|
||||||
|
if (parsed >= 0) parsed.toString() else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to parse local asset id from content URI: $uri", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryParseLastPathSegmentId(uri: Uri): String? {
|
||||||
|
val segment = uri.lastPathSegment ?: return null
|
||||||
|
return if (segment.all(Char::isDigit)) segment else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyUriToTempFile(context: Context, uri: Uri, mimeType: String): File? {
|
||||||
|
return try {
|
||||||
|
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
||||||
|
val tempFile = File.createTempFile("view_intent_", extension, context.cacheDir)
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
FileOutputStream(tempFile).use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
} ?: return null
|
||||||
|
tempFile
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveLocalIdByNameAndSize(context: Context, uri: Uri, mimeType: String): String? {
|
||||||
|
val metaProjection = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||||
|
val (displayName, size) =
|
||||||
|
try {
|
||||||
|
context.contentResolver.query(uri, metaProjection, null, null, null)?.use { cursor ->
|
||||||
|
if (!cursor.moveToFirst()) return null
|
||||||
|
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
val name = if (nameIdx >= 0) cursor.getString(nameIdx) else null
|
||||||
|
val bytes = if (sizeIdx >= 0) cursor.getLong(sizeIdx) else -1L
|
||||||
|
if (name.isNullOrBlank() || bytes < 0) return null
|
||||||
|
name to bytes
|
||||||
|
} ?: return null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val tableUri = when {
|
||||||
|
mimeType.startsWith("image/") -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
mimeType.startsWith("video/") -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
|
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
} else {
|
||||||
|
MediaStore.Files.getContentUri("external")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
context.contentResolver
|
||||||
|
.query(
|
||||||
|
tableUri,
|
||||||
|
arrayOf(MediaStore.MediaColumns._ID),
|
||||||
|
"${MediaStore.MediaColumns.DISPLAY_NAME}=? AND ${MediaStore.MediaColumns.SIZE}=?",
|
||||||
|
arrayOf(displayName, size.toString()),
|
||||||
|
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC",
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (!cursor.moveToFirst()) return null
|
||||||
|
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
|
||||||
|
if (idIndex < 0) return null
|
||||||
|
cursor.getLong(idIndex).toString()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||||
|
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
|
||||||
|
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
|
||||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
@@ -105,6 +107,8 @@
|
|||||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||||
|
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
|
||||||
|
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||||
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -283,6 +287,7 @@
|
|||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
|
B2EE00052E72CA15008B6CA7 /* Permission */,
|
||||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
@@ -317,6 +322,15 @@
|
|||||||
path = Connectivity;
|
path = Connectivity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
B2EE00052E72CA15008B6CA7 /* Permission */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */,
|
||||||
|
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */,
|
||||||
|
);
|
||||||
|
path = Permission;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -619,6 +633,8 @@
|
|||||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||||
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
|
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
|
||||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
||||||
|
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */,
|
||||||
|
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */,
|
||||||
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
|
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
||||||
@@ -718,6 +734,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share.profile;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -750,7 +767,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -801,6 +817,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share.debug;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@@ -860,6 +877,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -894,7 +912,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -924,7 +941,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -1080,7 +1096,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -1124,7 +1139,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -1165,7 +1179,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 240;
|
CURRENT_PROJECT_VERSION = 240;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
|
||||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import native_video_player
|
|||||||
|
|
||||||
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
||||||
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
||||||
|
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
|
||||||
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
||||||
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
||||||
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
|
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
|
||||||
|
|||||||
Generated
+14
@@ -288,6 +288,7 @@ protocol NetworkApi {
|
|||||||
func hasCertificate() throws -> Bool
|
func hasCertificate() throws -> Bool
|
||||||
func getClientPointer() throws -> Int64
|
func getClientPointer() throws -> Int64
|
||||||
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
|
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
|
||||||
|
func getAppGroupId() throws -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -388,5 +389,18 @@ class NetworkApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
setRequestHeadersChannel.setMessageHandler(nil)
|
setRequestHeadersChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
|
let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
getAppGroupIdChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.getAppGroupId()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getAppGroupIdChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ class NetworkApiImpl: NetworkApi {
|
|||||||
return Int64(Int(bitPattern: pointer))
|
return Int64(Int(bitPattern: pointer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAppGroupId() throws -> String {
|
||||||
|
return Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||||
|
}
|
||||||
|
|
||||||
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
|
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
|
||||||
URLSessionManager.setServerUrls(serverUrls)
|
URLSessionManager.setServerUrls(serverUrls)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import native_video_player
|
|||||||
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
|
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
|
||||||
let HEADERS_KEY = "immich.request_headers"
|
let HEADERS_KEY = "immich.request_headers"
|
||||||
let SERVER_URLS_KEY = "immich.server_urls"
|
let SERVER_URLS_KEY = "immich.server_urls"
|
||||||
let APP_GROUP = "group.app.immich.share"
|
let APP_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||||
let COOKIE_EXPIRY_DAYS: TimeInterval = 400
|
let COOKIE_EXPIRY_DAYS: TimeInterval = 400
|
||||||
|
|
||||||
enum AuthCookie: CaseIterable {
|
enum AuthCookie: CaseIterable {
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
// 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?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
protocol PermissionApi {
|
||||||
|
func hasManageMediaPermission() throws -> Bool
|
||||||
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class PermissionApiSetup {
|
||||||
|
static var codec: FlutterStandardMessageCodec { FlutterStandardMessageCodec.sharedInstance() }
|
||||||
|
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
||||||
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
|
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.hasManageMediaPermission()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasManageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let requestManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
requestManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
api.requestManageMediaPermission { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestManageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let manageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
manageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
api.manageMediaPermission { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class PermissionApiImpl: PermissionApi {
|
||||||
|
func hasManageMediaPermission() throws -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(.success(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(.success(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.immich.share</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.immich.share</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Generated
+19
@@ -537,6 +537,7 @@ protocol NativeSyncApi {
|
|||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||||
func cancelHashing() throws
|
func cancelHashing() throws
|
||||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||||
|
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,6 +722,24 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
getTrashedAssetsChannel.setMessageHandler(nil)
|
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
|
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)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
|
|
||||||
var domainAlbum = PlatformAlbum(
|
var domainAlbum = PlatformAlbum(
|
||||||
id: album.localIdentifier,
|
id: album.localIdentifier,
|
||||||
name: album.localizedTitle!,
|
name: album.localizedTitle ?? album.localIdentifier,
|
||||||
updatedAt: nil,
|
updatedAt: nil,
|
||||||
isCloud: isCloud,
|
isCloud: isCloud,
|
||||||
assetCount: Int64(assets.count)
|
assetCount: Int64(assets.count)
|
||||||
@@ -383,6 +383,10 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
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> {
|
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||||
// Ensure to actually getting all assets for the Recents album
|
// Ensure to actually getting all assets for the Recents album
|
||||||
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
|
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.immich.share</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
let IMMICH_SHARE_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||||
|
|
||||||
enum WidgetError: Error, Codable {
|
enum WidgetError: Error, Codable {
|
||||||
case noLogin
|
case noLogin
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>AppGroupId</key>
|
||||||
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.immich.share</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -21,6 +21,7 @@ platform :ios do
|
|||||||
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
|
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
|
||||||
BASE_BUNDLE_ID = "app.alextran.immich"
|
BASE_BUNDLE_ID = "app.alextran.immich"
|
||||||
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
|
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
|
||||||
|
DEV_GROUP_ID = "group.app.immich.share.testflight"
|
||||||
|
|
||||||
# Helper method to get App Store Connect API key
|
# Helper method to get App Store Connect API key
|
||||||
def get_api_key
|
def get_api_key
|
||||||
@@ -33,6 +34,13 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override
|
||||||
|
def build_xcargs(group_id: nil)
|
||||||
|
args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual"
|
||||||
|
args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id
|
||||||
|
args
|
||||||
|
end
|
||||||
|
|
||||||
# Helper method to get version from pubspec.yaml
|
# Helper method to get version from pubspec.yaml
|
||||||
def get_version_from_pubspec
|
def get_version_from_pubspec
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
@@ -89,7 +97,8 @@ end
|
|||||||
version_number: nil,
|
version_number: nil,
|
||||||
profile_name_main:,
|
profile_name_main:,
|
||||||
profile_name_share:,
|
profile_name_share:,
|
||||||
profile_name_widget:
|
profile_name_widget:,
|
||||||
|
group_id: nil
|
||||||
)
|
)
|
||||||
app_identifier = base_bundle_id
|
app_identifier = base_bundle_id
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ end
|
|||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
export_method: "app-store",
|
export_method: "app-store",
|
||||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
xcargs: build_xcargs(group_id: group_id),
|
||||||
export_options: {
|
export_options: {
|
||||||
provisioningProfiles: {
|
provisioningProfiles: {
|
||||||
"#{app_identifier}" => profile_name_main,
|
"#{app_identifier}" => profile_name_main,
|
||||||
@@ -165,7 +174,8 @@ end
|
|||||||
distribute_external: false,
|
distribute_external: false,
|
||||||
profile_name_main: main_profile_name,
|
profile_name_main: main_profile_name,
|
||||||
profile_name_share: share_profile_name,
|
profile_name_share: share_profile_name,
|
||||||
profile_name_widget: widget_profile_name
|
profile_name_widget: widget_profile_name,
|
||||||
|
group_id: DEV_GROUP_ID
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -274,7 +284,7 @@ end
|
|||||||
configuration: "Release",
|
configuration: "Release",
|
||||||
export_method: "app-store",
|
export_method: "app-store",
|
||||||
skip_package_ipa: true,
|
skip_package_ipa: true,
|
||||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
xcargs: build_xcargs(group_id: DEV_GROUP_ID),
|
||||||
export_options: {
|
export_options: {
|
||||||
provisioningProfiles: {
|
provisioningProfiles: {
|
||||||
DEV_BUNDLE_ID => main_profile_name,
|
DEV_BUNDLE_ID => main_profile_name,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const int kTimelineAssetLoadBatchSize = 1024;
|
|||||||
const int kTimelineAssetLoadOppositeSize = 64;
|
const int kTimelineAssetLoadOppositeSize = 64;
|
||||||
|
|
||||||
// Widget keys
|
// Widget keys
|
||||||
const String appShareGroupId = "group.app.immich.share";
|
|
||||||
const String kWidgetAuthToken = "widget_auth_token";
|
const String kWidgetAuthToken = "widget_auth_token";
|
||||||
const String kWidgetServerEndpoint = "widget_server_url";
|
const String kWidgetServerEndpoint = "widget_server_url";
|
||||||
const String kWidgetCustomHeaders = "widget_custom_headers";
|
const String kWidgetCustomHeaders = "widget_custom_headers";
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ enum CleanupStep { selectDate, scan, delete }
|
|||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
enum AssetDateAggregation { start, end }
|
enum AssetDateAggregation { start, end }
|
||||||
|
|
||||||
|
enum SlideshowLook { contain, cover, blurredBackground }
|
||||||
|
|
||||||
|
enum SlideshowDirection { forward, backward, shuffle }
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
|
class AlbumConfig {
|
||||||
|
final AlbumSortMode sortMode;
|
||||||
|
final bool isReverse;
|
||||||
|
final bool isGrid;
|
||||||
|
|
||||||
|
const AlbumConfig({this.sortMode = AlbumSortMode.mostRecent, this.isReverse = true, this.isGrid = false});
|
||||||
|
|
||||||
|
AlbumConfig copyWith({AlbumSortMode? sortMode, bool? isReverse, bool? isGrid}) => AlbumConfig(
|
||||||
|
sortMode: sortMode ?? this.sortMode,
|
||||||
|
isReverse: isReverse ?? this.isReverse,
|
||||||
|
isGrid: isGrid ?? this.isGrid,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is AlbumConfig && other.sortMode == sortMode && other.isReverse == isReverse && other.isGrid == isGrid);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(sortMode, isReverse, isGrid);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AlbumConfig(sortMode: $sortMode, isReverse: $isReverse, isGrid: $isGrid)';
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/config/album_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/backup_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/image_config.dart';
|
import 'package:immich_mobile/domain/models/config/image_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/map_config.dart';
|
import 'package:immich_mobile/domain/models/config/map_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
||||||
@@ -12,6 +15,9 @@ class AppConfig {
|
|||||||
final TimelineConfig timeline;
|
final TimelineConfig timeline;
|
||||||
final ImageConfig image;
|
final ImageConfig image;
|
||||||
final ViewerConfig viewer;
|
final ViewerConfig viewer;
|
||||||
|
final SlideshowConfig slideshow;
|
||||||
|
final AlbumConfig album;
|
||||||
|
final BackupConfig backup;
|
||||||
|
|
||||||
const AppConfig({
|
const AppConfig({
|
||||||
this.theme = const .new(),
|
this.theme = const .new(),
|
||||||
@@ -20,6 +26,9 @@ class AppConfig {
|
|||||||
this.timeline = const .new(),
|
this.timeline = const .new(),
|
||||||
this.image = const .new(),
|
this.image = const .new(),
|
||||||
this.viewer = const .new(),
|
this.viewer = const .new(),
|
||||||
|
this.slideshow = const .new(),
|
||||||
|
this.album = const .new(),
|
||||||
|
this.backup = const .new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
AppConfig copyWith({
|
AppConfig copyWith({
|
||||||
@@ -29,6 +38,9 @@ class AppConfig {
|
|||||||
TimelineConfig? timeline,
|
TimelineConfig? timeline,
|
||||||
ImageConfig? image,
|
ImageConfig? image,
|
||||||
ViewerConfig? viewer,
|
ViewerConfig? viewer,
|
||||||
|
SlideshowConfig? slideshow,
|
||||||
|
AlbumConfig? album,
|
||||||
|
BackupConfig? backup,
|
||||||
}) => .new(
|
}) => .new(
|
||||||
theme: theme ?? this.theme,
|
theme: theme ?? this.theme,
|
||||||
cleanup: cleanup ?? this.cleanup,
|
cleanup: cleanup ?? this.cleanup,
|
||||||
@@ -36,6 +48,9 @@ class AppConfig {
|
|||||||
timeline: timeline ?? this.timeline,
|
timeline: timeline ?? this.timeline,
|
||||||
image: image ?? this.image,
|
image: image ?? this.image,
|
||||||
viewer: viewer ?? this.viewer,
|
viewer: viewer ?? this.viewer,
|
||||||
|
slideshow: slideshow ?? this.slideshow,
|
||||||
|
album: album ?? this.album,
|
||||||
|
backup: backup ?? this.backup,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -47,12 +62,15 @@ class AppConfig {
|
|||||||
other.map == map &&
|
other.map == map &&
|
||||||
other.timeline == timeline &&
|
other.timeline == timeline &&
|
||||||
other.image == image &&
|
other.image == image &&
|
||||||
other.viewer == viewer);
|
other.viewer == viewer &&
|
||||||
|
other.slideshow == slideshow &&
|
||||||
|
other.album == album &&
|
||||||
|
other.backup == backup);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer);
|
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)';
|
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class BackupConfig {
|
||||||
|
final bool enabled;
|
||||||
|
final bool useCellularForVideos;
|
||||||
|
final bool useCellularForPhotos;
|
||||||
|
final bool requireCharging;
|
||||||
|
final int triggerDelay;
|
||||||
|
final bool syncAlbums;
|
||||||
|
|
||||||
|
const BackupConfig({
|
||||||
|
this.enabled = false,
|
||||||
|
this.useCellularForVideos = false,
|
||||||
|
this.useCellularForPhotos = false,
|
||||||
|
this.requireCharging = false,
|
||||||
|
this.triggerDelay = 30,
|
||||||
|
this.syncAlbums = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
BackupConfig copyWith({
|
||||||
|
bool? enabled,
|
||||||
|
bool? useCellularForVideos,
|
||||||
|
bool? useCellularForPhotos,
|
||||||
|
bool? requireCharging,
|
||||||
|
int? triggerDelay,
|
||||||
|
bool? syncAlbums,
|
||||||
|
}) => BackupConfig(
|
||||||
|
enabled: enabled ?? this.enabled,
|
||||||
|
useCellularForVideos: useCellularForVideos ?? this.useCellularForVideos,
|
||||||
|
useCellularForPhotos: useCellularForPhotos ?? this.useCellularForPhotos,
|
||||||
|
requireCharging: requireCharging ?? this.requireCharging,
|
||||||
|
triggerDelay: triggerDelay ?? this.triggerDelay,
|
||||||
|
syncAlbums: syncAlbums ?? this.syncAlbums,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is BackupConfig &&
|
||||||
|
other.enabled == enabled &&
|
||||||
|
other.useCellularForVideos == useCellularForVideos &&
|
||||||
|
other.useCellularForPhotos == useCellularForPhotos &&
|
||||||
|
other.requireCharging == requireCharging &&
|
||||||
|
other.triggerDelay == triggerDelay &&
|
||||||
|
other.syncAlbums == syncAlbums);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(enabled, useCellularForVideos, useCellularForPhotos, requireCharging, triggerDelay, syncAlbums);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'BackupConfig(enabled: $enabled, useCellularForVideos: $useCellularForVideos, useCellularForPhotos: $useCellularForPhotos, requireCharging: $requireCharging, triggerDelay: $triggerDelay, syncAlbums: $syncAlbums)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class NetworkConfig {
|
||||||
|
final bool autoEndpointSwitching;
|
||||||
|
final String? preferredWifiName;
|
||||||
|
final String? localEndpoint;
|
||||||
|
final List<String> externalEndpointList;
|
||||||
|
final Map<String, String> customHeaders;
|
||||||
|
|
||||||
|
const NetworkConfig({
|
||||||
|
this.autoEndpointSwitching = false,
|
||||||
|
this.preferredWifiName,
|
||||||
|
this.localEndpoint,
|
||||||
|
this.externalEndpointList = const [],
|
||||||
|
this.customHeaders = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
NetworkConfig copyWith({
|
||||||
|
bool? autoEndpointSwitching,
|
||||||
|
String? preferredWifiName,
|
||||||
|
String? localEndpoint,
|
||||||
|
List<String>? externalEndpointList,
|
||||||
|
Map<String, String>? customHeaders,
|
||||||
|
}) => NetworkConfig(
|
||||||
|
autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching,
|
||||||
|
preferredWifiName: preferredWifiName ?? this.preferredWifiName,
|
||||||
|
localEndpoint: localEndpoint ?? this.localEndpoint,
|
||||||
|
externalEndpointList: externalEndpointList ?? this.externalEndpointList,
|
||||||
|
customHeaders: customHeaders ?? this.customHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is NetworkConfig &&
|
||||||
|
other.autoEndpointSwitching == autoEndpointSwitching &&
|
||||||
|
other.preferredWifiName == preferredWifiName &&
|
||||||
|
other.localEndpoint == localEndpoint &&
|
||||||
|
listEquals(other.externalEndpointList, externalEndpointList) &&
|
||||||
|
mapEquals(other.customHeaders, customHeaders));
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
autoEndpointSwitching,
|
||||||
|
preferredWifiName,
|
||||||
|
localEndpoint,
|
||||||
|
Object.hashAll(externalEndpointList),
|
||||||
|
Object.hashAllUnordered(customHeaders.entries.map((e) => Object.hash(e.key, e.value))),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'NetworkConfig(autoEndpointSwitching: $autoEndpointSwitching, preferredWifiName: $preferredWifiName, localEndpoint: $localEndpoint, externalEndpointList: $externalEndpointList, customHeaders: $customHeaders)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
|
||||||
|
class SlideshowConfig {
|
||||||
|
final bool transition;
|
||||||
|
final bool repeat;
|
||||||
|
final int duration;
|
||||||
|
final SlideshowLook look;
|
||||||
|
final SlideshowDirection direction;
|
||||||
|
|
||||||
|
const SlideshowConfig({
|
||||||
|
this.transition = true,
|
||||||
|
this.repeat = true,
|
||||||
|
this.duration = 5,
|
||||||
|
this.look = SlideshowLook.contain,
|
||||||
|
this.direction = SlideshowDirection.forward,
|
||||||
|
});
|
||||||
|
|
||||||
|
SlideshowConfig copyWith({
|
||||||
|
bool? transition,
|
||||||
|
bool? repeat,
|
||||||
|
int? duration,
|
||||||
|
SlideshowLook? look,
|
||||||
|
SlideshowDirection? direction,
|
||||||
|
}) => SlideshowConfig(
|
||||||
|
transition: transition ?? this.transition,
|
||||||
|
repeat: repeat ?? this.repeat,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
look: look ?? this.look,
|
||||||
|
direction: direction ?? this.direction,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is SlideshowConfig &&
|
||||||
|
other.transition == transition &&
|
||||||
|
other.repeat == repeat &&
|
||||||
|
other.duration == duration &&
|
||||||
|
other.look == look &&
|
||||||
|
other.direction == direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(transition, repeat, duration, look, direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SlideshowConfig(transition: $transition, repeat: $repeat, duration: $duration, look: $look, direction: $direction)';
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/config/network_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
|
||||||
class SystemConfig {
|
class SystemConfig {
|
||||||
final LogLevel logLevel;
|
final LogLevel logLevel;
|
||||||
|
final NetworkConfig network;
|
||||||
|
|
||||||
const SystemConfig({this.logLevel = .info});
|
const SystemConfig({this.logLevel = .info, this.network = const .new()});
|
||||||
|
|
||||||
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
|
SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) =>
|
||||||
|
SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => logLevel.hashCode;
|
int get hashCode => Object.hash(logLevel, network);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfig(logLevel: $logLevel)';
|
String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/config/app_config.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
enum MetadataDomain<T extends Object> {
|
enum MetadataDomain<T extends Object> {
|
||||||
appConfig<AppConfig>('config.app'),
|
appConfig<AppConfig>('config.app'),
|
||||||
@@ -34,6 +35,41 @@ enum MetadataKey<T extends Object> {
|
|||||||
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
|
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
|
||||||
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
|
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
|
||||||
|
|
||||||
|
// Network
|
||||||
|
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
|
||||||
|
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
|
||||||
|
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
|
||||||
|
networkExternalEndpointList<List<String>>(
|
||||||
|
.systemConfig,
|
||||||
|
'network.externalEndpointList',
|
||||||
|
[],
|
||||||
|
_ListCodec(_PrimitiveCodec.string),
|
||||||
|
),
|
||||||
|
networkCustomHeaders<Map<String, String>>(
|
||||||
|
.systemConfig,
|
||||||
|
'network.customHeaders',
|
||||||
|
{},
|
||||||
|
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Album
|
||||||
|
albumSortMode<AlbumSortMode>(
|
||||||
|
.appConfig,
|
||||||
|
'album.sortMode',
|
||||||
|
AlbumSortMode.mostRecent,
|
||||||
|
_EnumCodec(AlbumSortMode.values),
|
||||||
|
),
|
||||||
|
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
|
||||||
|
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
|
||||||
|
|
||||||
|
// Backup
|
||||||
|
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
|
||||||
|
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
|
||||||
|
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
|
||||||
|
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
|
||||||
|
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
|
||||||
|
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
|
||||||
|
|
||||||
// Timeline
|
// Timeline
|
||||||
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
||||||
timelineGroupAssetsBy<GroupAssetsBy>(
|
timelineGroupAssetsBy<GroupAssetsBy>(
|
||||||
@@ -64,7 +100,19 @@ enum MetadataKey<T extends Object> {
|
|||||||
),
|
),
|
||||||
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
||||||
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
||||||
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false);
|
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false),
|
||||||
|
|
||||||
|
// Slideshow
|
||||||
|
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
|
||||||
|
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
|
||||||
|
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
|
||||||
|
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
|
||||||
|
slideshowDirection<SlideshowDirection>(
|
||||||
|
.appConfig,
|
||||||
|
'slideshow.direction',
|
||||||
|
SlideshowDirection.forward,
|
||||||
|
_EnumCodec(SlideshowDirection.values),
|
||||||
|
);
|
||||||
|
|
||||||
final MetadataDomain domain;
|
final MetadataDomain domain;
|
||||||
final String name;
|
final String name;
|
||||||
@@ -131,6 +179,47 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
|||||||
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
||||||
|
final _MetadataCodec<K> _keyCodec;
|
||||||
|
final _MetadataCodec<V> _valueCodec;
|
||||||
|
|
||||||
|
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(Map<K, V> value) {
|
||||||
|
final entries = <String, String>{};
|
||||||
|
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||||
|
return jsonEncode(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<K, V>? decode(String raw) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final result = <K, V>{};
|
||||||
|
for (final entry in decoded.entries) {
|
||||||
|
final rawKey = entry.key;
|
||||||
|
final rawValue = entry.value;
|
||||||
|
if (rawKey is! String || rawValue is! String) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final k = _keyCodec.decode(rawKey);
|
||||||
|
final v = _valueCodec.decode(rawValue);
|
||||||
|
if (k == null || v == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
result[k] = v;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} on FormatException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
||||||
final _MetadataCodec<T> _elementCodec;
|
final _MetadataCodec<T> _elementCodec;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
|
||||||
enum Setting<T> {
|
enum Setting<T> {
|
||||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false);
|
||||||
enableBackup<bool>(StoreKey.enableBackup, false);
|
|
||||||
|
|
||||||
const Setting(this.storeKey, this.defaultValue);
|
const Setting(this.storeKey, this.defaultValue);
|
||||||
|
|
||||||
|
|||||||
@@ -6,36 +6,33 @@ enum StoreKey<T> {
|
|||||||
version<int>._(0),
|
version<int>._(0),
|
||||||
currentUser<UserDto>._(2),
|
currentUser<UserDto>._(2),
|
||||||
deviceId<String>._(4),
|
deviceId<String>._(4),
|
||||||
backupRequireCharging<bool>._(7),
|
|
||||||
backupTriggerDelay<int>._(8),
|
|
||||||
serverUrl<String>._(10),
|
serverUrl<String>._(10),
|
||||||
accessToken<String>._(11),
|
accessToken<String>._(11),
|
||||||
serverEndpoint<String>._(12),
|
serverEndpoint<String>._(12),
|
||||||
selectedAlbumSortOrder<int>._(113),
|
|
||||||
advancedTroubleshooting<bool>._(114),
|
advancedTroubleshooting<bool>._(114),
|
||||||
selectedAlbumSortReverse<bool>._(123),
|
|
||||||
enableHapticFeedback<bool>._(126),
|
enableHapticFeedback<bool>._(126),
|
||||||
customHeaders<String>._(127),
|
|
||||||
syncAlbums<bool>._(131),
|
|
||||||
|
|
||||||
// Auto endpoint switching
|
|
||||||
autoEndpointSwitching<bool>._(132),
|
|
||||||
preferredWifiName<String>._(133),
|
|
||||||
localEndpoint<String>._(134),
|
|
||||||
externalEndpointList<String>._(135),
|
|
||||||
|
|
||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
// Read-only Mode settings
|
// Read-only Mode settings
|
||||||
readonlyModeEnabled<bool>._(138),
|
readonlyModeEnabled<bool>._(138),
|
||||||
albumGridView<bool>._(140),
|
|
||||||
|
|
||||||
// Experimental stuff
|
|
||||||
enableBackup<bool>._(1003),
|
|
||||||
useWifiForUploadVideos<bool>._(1004),
|
|
||||||
useWifiForUploadPhotos<bool>._(1005),
|
|
||||||
syncMigrationStatus<String>._(1013),
|
syncMigrationStatus<String>._(1013),
|
||||||
|
|
||||||
// Legacy keys that have been migrated to the new metadata store
|
// Legacy keys that have been migrated to the new metadata store
|
||||||
|
legacyBackupRequireCharging<bool>._(7),
|
||||||
|
legacyBackupTriggerDelay<int>._(8),
|
||||||
|
legacySyncAlbums<bool>._(131),
|
||||||
|
legacyEnableBackup<bool>._(1003),
|
||||||
|
legacyUseWifiForUploadVideos<bool>._(1004),
|
||||||
|
legacyUseWifiForUploadPhotos<bool>._(1005),
|
||||||
|
legacySelectedAlbumSortOrder<int>._(113),
|
||||||
|
legacySelectedAlbumSortReverse<bool>._(123),
|
||||||
|
legacyAlbumGridView<bool>._(140),
|
||||||
|
legacyAutoEndpointSwitching<bool>._(132),
|
||||||
|
legacyPreferredWifiName<String>._(133),
|
||||||
|
legacyLocalEndpoint<String>._(134),
|
||||||
|
legacyExternalEndpointList<String>._(135),
|
||||||
|
legacyCustomHeaders<String>._(127),
|
||||||
legacyLoopVideo<bool>._(117),
|
legacyLoopVideo<bool>._(117),
|
||||||
legacyLoadOriginalVideo<bool>._(136),
|
legacyLoadOriginalVideo<bool>._(136),
|
||||||
legacyAutoPlayVideo<bool>._(139),
|
legacyAutoPlayVideo<bool>._(139),
|
||||||
|
|||||||
@@ -11,15 +11,14 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/services/auth.service.dart';
|
import 'package:immich_mobile/services/auth.service.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
@@ -39,16 +38,15 @@ class BackgroundWorkerFgService {
|
|||||||
Future<void> saveNotificationMessage(String title, String body) =>
|
Future<void> saveNotificationMessage(String title, String body) =>
|
||||||
_foregroundHostApi.saveNotificationMessage(title, body);
|
_foregroundHostApi.saveNotificationMessage(title, body);
|
||||||
|
|
||||||
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure(
|
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
|
||||||
BackgroundWorkerSettings(
|
final backup = MetadataRepository.instance.appConfig.backup;
|
||||||
minimumDelaySeconds:
|
return _foregroundHostApi.configure(
|
||||||
minimumDelaySeconds ??
|
BackgroundWorkerSettings(
|
||||||
Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue),
|
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
|
||||||
requiresCharging:
|
requiresCharging: requireCharging ?? backup.requireCharging,
|
||||||
requireCharging ??
|
),
|
||||||
Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue),
|
);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> disable() => _foregroundHostApi.disable();
|
Future<void> disable() => _foregroundHostApi.disable();
|
||||||
}
|
}
|
||||||
@@ -71,7 +69,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
BackgroundWorkerFlutterApi.setUp(this);
|
BackgroundWorkerFlutterApi.setUp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false;
|
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/extensions/platform_extensions.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_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/trashed_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:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.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/datetime_helpers.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -23,29 +23,29 @@ class LocalSyncService {
|
|||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final StorageRepository _storageRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
LocalSyncService({
|
LocalSyncService({
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
required DriftLocalAlbumRepository localAlbumRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required DriftLocalAssetRepository localAssetRepository,
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required AssetMediaRepository assetMediaRepository,
|
||||||
required StorageRepository storageRepository,
|
required IPermissionRepository permissionRepository,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required NativeSyncApi nativeSyncApi,
|
||||||
}) : _localAlbumRepository = localAlbumRepository,
|
}) : _localAlbumRepository = localAlbumRepository,
|
||||||
_localAssetRepository = localAssetRepository,
|
_localAssetRepository = localAssetRepository,
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
_localFilesManager = localFilesManager,
|
_assetMediaRepository = assetMediaRepository,
|
||||||
_storageRepository = storageRepository,
|
_permissionRepository = permissionRepository,
|
||||||
_nativeSyncApi = nativeSyncApi;
|
_nativeSyncApi = nativeSyncApi;
|
||||||
|
|
||||||
Future<void> sync({bool full = false}) async {
|
Future<void> sync({bool full = false}) async {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
final hasPermission = await _localFilesManager.hasManageMediaPermission();
|
final hasPermission = await _permissionRepository.hasManageMediaPermission();
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
await _syncTrashedAssets();
|
await _syncTrashedAssets();
|
||||||
} else {
|
} else {
|
||||||
@@ -373,7 +373,7 @@ class LocalSyncService {
|
|||||||
|
|
||||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||||
if (assetsToRestore.isNotEmpty) {
|
if (assetsToRestore.isNotEmpty) {
|
||||||
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore);
|
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||||
} else {
|
} else {
|
||||||
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
||||||
@@ -381,15 +381,15 @@ class LocalSyncService {
|
|||||||
|
|
||||||
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
||||||
if (localAssetsToTrash.isNotEmpty) {
|
if (localAssetsToTrash.isNotEmpty) {
|
||||||
final mediaUrls = await Future.wait(
|
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||||
localAssetsToTrash.values
|
_log.info("Moving to trash ${localIds.join(", ")} assets");
|
||||||
.expand((e) => e)
|
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
if (movedIds.isNotEmpty) {
|
||||||
);
|
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||||
_log.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
)..removeWhere((_, assets) => assets.isEmpty);
|
||||||
if (result) {
|
|
||||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
|
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
|
||||||
|
|||||||
@@ -9,12 +9,47 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor
|
|||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// Categorizes a heterogeneous asset selection into the candidates that can
|
||||||
|
/// be added to an album immediately (already on the server) and the local-only
|
||||||
|
/// candidates that must be uploaded first.
|
||||||
|
class AlbumAssetCandidates {
|
||||||
|
final List<String> remoteAssetIds;
|
||||||
|
final List<LocalAsset> localAssetsToUpload;
|
||||||
|
|
||||||
|
const AlbumAssetCandidates({required this.remoteAssetIds, required this.localAssetsToUpload});
|
||||||
|
}
|
||||||
|
|
||||||
class RemoteAlbumService {
|
class RemoteAlbumService {
|
||||||
|
static final _logger = Logger('RemoteAlbumService');
|
||||||
|
|
||||||
final DriftRemoteAlbumRepository _repository;
|
final DriftRemoteAlbumRepository _repository;
|
||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
final ForegroundUploadService _uploadService;
|
||||||
|
|
||||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
const RemoteAlbumService(this._repository, this._albumApiRepository, this._uploadService);
|
||||||
|
|
||||||
|
/// Categorizes a heterogeneous asset selection into already-on-server IDs
|
||||||
|
/// and local assets that still need to be uploaded.
|
||||||
|
static AlbumAssetCandidates categorizeCandidates(Iterable<BaseAsset> assets) {
|
||||||
|
final remoteIds = <String>[];
|
||||||
|
final localToUpload = <LocalAsset>[];
|
||||||
|
for (final asset in assets) {
|
||||||
|
if (asset is RemoteAsset) {
|
||||||
|
remoteIds.add(asset.id);
|
||||||
|
} else if (asset is LocalAsset) {
|
||||||
|
final remoteId = asset.remoteId;
|
||||||
|
if (remoteId != null) {
|
||||||
|
remoteIds.add(remoteId);
|
||||||
|
} else {
|
||||||
|
localToUpload.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AlbumAssetCandidates(remoteAssetIds: remoteIds, localAssetsToUpload: localToUpload);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
return _repository.watchAlbum(albumId);
|
return _repository.watchAlbum(albumId);
|
||||||
@@ -148,6 +183,122 @@ class RemoteAlbumService {
|
|||||||
return album.added.length;
|
return album.added.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// !TODO The name here is not clear as we have addAssets method above,
|
||||||
|
/// which is only add remote assets to album, for the next PR, we will allow
|
||||||
|
/// adding local assets from album from the timeline as well with this flow.
|
||||||
|
/// So saving that for the next refactor
|
||||||
|
Future<int> addAssetsToAlbum({
|
||||||
|
required String albumId,
|
||||||
|
required UserDto uploader,
|
||||||
|
required AlbumAssetCandidates candidates,
|
||||||
|
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||||
|
}) async {
|
||||||
|
int addedCount = 0;
|
||||||
|
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||||
|
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
||||||
|
}
|
||||||
|
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||||
|
addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks);
|
||||||
|
}
|
||||||
|
return addedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an album, seeding it with already-remote asset IDs, then uploads
|
||||||
|
/// local-only assets and links each one as it finishes.
|
||||||
|
Future<RemoteAlbum> createAlbumWithAssets({
|
||||||
|
required String title,
|
||||||
|
required UserDto owner,
|
||||||
|
String? description,
|
||||||
|
AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []),
|
||||||
|
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||||
|
}) async {
|
||||||
|
final album = await createAlbum(
|
||||||
|
title: title,
|
||||||
|
owner: owner,
|
||||||
|
description: description,
|
||||||
|
assetIds: candidates.remoteAssetIds,
|
||||||
|
);
|
||||||
|
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||||
|
await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks);
|
||||||
|
}
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _uploadAndAddLocals(
|
||||||
|
String albumId,
|
||||||
|
UserDto uploader,
|
||||||
|
List<LocalAsset> localAssets,
|
||||||
|
UploadCallbacks userCallbacks,
|
||||||
|
) async {
|
||||||
|
int addedCount = 0;
|
||||||
|
final pendingAdds = <Future<void>>[];
|
||||||
|
final localById = {for (final a in localAssets) a.id: a};
|
||||||
|
|
||||||
|
final wrappedCallbacks = UploadCallbacks(
|
||||||
|
onProgress: (localId, filename, bytes, totalBytes) => _runUploadCallback(
|
||||||
|
'Upload progress callback failed for $localId',
|
||||||
|
() => userCallbacks.onProgress?.call(localId, filename, bytes, totalBytes),
|
||||||
|
),
|
||||||
|
onICloudProgress: (localId, progress) => _runUploadCallback(
|
||||||
|
'iCloud progress callback failed for $localId',
|
||||||
|
() => userCallbacks.onICloudProgress?.call(localId, progress),
|
||||||
|
),
|
||||||
|
onError: (localId, errorMessage) => _runUploadCallback(
|
||||||
|
'Upload error callback failed for $localId',
|
||||||
|
() => userCallbacks.onError?.call(localId, errorMessage),
|
||||||
|
),
|
||||||
|
onSuccess: (localId, remoteId) {
|
||||||
|
_runUploadCallback(
|
||||||
|
'Upload success callback failed for $localId',
|
||||||
|
() => userCallbacks.onSuccess?.call(localId, remoteId),
|
||||||
|
);
|
||||||
|
final source = localById[localId];
|
||||||
|
if (source == null) {
|
||||||
|
_logger.warning('Upload success for $localId but source LocalAsset missing; skipping album link');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAdds.add(
|
||||||
|
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||||
|
.then<void>((added) {
|
||||||
|
addedCount += added;
|
||||||
|
})
|
||||||
|
.catchError((Object error, StackTrace stack) {
|
||||||
|
_logger.warning('Failed to add uploaded asset $remoteId to album $albumId', error, stack);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks);
|
||||||
|
await Future.wait(pendingAdds);
|
||||||
|
return addedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _runUploadCallback(String message, void Function() callback) {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.warning(message, error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Links a freshly-uploaded asset to an album, ensuring the local DB
|
||||||
|
/// reflects the change without waiting for the next sync. We call the API
|
||||||
|
/// (server is the source of truth), then upsert a placeholder
|
||||||
|
/// `remote_asset_entity` row from the local source so the FK-protected
|
||||||
|
/// junction insert succeeds. Sync overwrites the placeholder later with
|
||||||
|
/// the authoritative server data.
|
||||||
|
Future<int> _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
|
||||||
|
final result = await _albumApiRepository.addAssets(albumId, [remoteId]);
|
||||||
|
if (result.added.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _repository.upsertRemoteAssetStub(remoteId: remoteId, ownerId: uploader.id, source: source);
|
||||||
|
await _repository.addAssets(albumId, result.added);
|
||||||
|
return result.added.length;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbum(String albumId) async {
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
await _albumApiRepository.deleteAlbum(albumId);
|
await _albumApiRepository.deleteAlbum(albumId);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.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/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.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_migration.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.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/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -34,8 +34,8 @@ class SyncStreamService {
|
|||||||
final SyncStreamRepository _syncStreamRepository;
|
final SyncStreamRepository _syncStreamRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final StorageRepository _storageRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final SyncMigrationRepository _syncMigrationRepository;
|
final SyncMigrationRepository _syncMigrationRepository;
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
final bool Function()? _cancelChecker;
|
final bool Function()? _cancelChecker;
|
||||||
@@ -45,8 +45,8 @@ class SyncStreamService {
|
|||||||
required SyncStreamRepository syncStreamRepository,
|
required SyncStreamRepository syncStreamRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required DriftLocalAssetRepository localAssetRepository,
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required AssetMediaRepository assetMediaRepository,
|
||||||
required StorageRepository storageRepository,
|
required IPermissionRepository permissionRepository,
|
||||||
required SyncMigrationRepository syncMigrationRepository,
|
required SyncMigrationRepository syncMigrationRepository,
|
||||||
required ApiService api,
|
required ApiService api,
|
||||||
bool Function()? cancelChecker,
|
bool Function()? cancelChecker,
|
||||||
@@ -54,8 +54,8 @@ class SyncStreamService {
|
|||||||
_syncStreamRepository = syncStreamRepository,
|
_syncStreamRepository = syncStreamRepository,
|
||||||
_localAssetRepository = localAssetRepository,
|
_localAssetRepository = localAssetRepository,
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
_localFilesManager = localFilesManager,
|
_assetMediaRepository = assetMediaRepository,
|
||||||
_storageRepository = storageRepository,
|
_permissionRepository = permissionRepository,
|
||||||
_syncMigrationRepository = syncMigrationRepository,
|
_syncMigrationRepository = syncMigrationRepository,
|
||||||
_api = api,
|
_api = api,
|
||||||
_cancelChecker = cancelChecker;
|
_cancelChecker = cancelChecker;
|
||||||
@@ -500,22 +500,22 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
||||||
final mediaUrls = await Future.wait(
|
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||||
localAssetsToTrash.values
|
_logger.info("Moving to trash ${localIds.join(", ")} assets");
|
||||||
.expand((e) => e)
|
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
if (movedIds.isNotEmpty) {
|
||||||
);
|
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
)..removeWhere((_, assets) => assets.isEmpty);
|
||||||
if (result) {
|
|
||||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyRemoteRestoreToLocal() async {
|
Future<void> _applyRemoteRestoreToLocal() async {
|
||||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||||
if (assetsToRestore.isNotEmpty) {
|
if (assetsToRestore.isNotEmpty) {
|
||||||
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore);
|
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||||
} else {
|
} else {
|
||||||
_logger.info("No remote assets found for restoration");
|
_logger.info("No remote assets found for restoration");
|
||||||
@@ -523,7 +523,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
||||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||||
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -533,7 +533,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
||||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||||
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart';
|
||||||
|
|
||||||
|
final tagServiceProvider = Provider<TagService>((ref) => TagService(ref.watch(tagsApiRepositoryProvider)));
|
||||||
|
|
||||||
|
class TagService {
|
||||||
|
final TagsApiRepository _repository;
|
||||||
|
|
||||||
|
const TagService(this._repository);
|
||||||
|
|
||||||
|
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||||
|
return _repository.bulkTagAssets(assetIds, tagIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<Tag>> getAllTags() async {
|
||||||
|
final dtos = await _repository.getAllTags();
|
||||||
|
if (dtos == null) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return dtos.map((dto) => Tag.fromDto(dto)).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Tag>> upsertTags(List<String> tags) async {
|
||||||
|
final dtos = await _repository.upsertTags(tags);
|
||||||
|
if (dtos == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return dtos.map((dto) => Tag.fromDto(dto)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ typedef TimelineBucketSource = Stream<List<Bucket>> Function();
|
|||||||
|
|
||||||
typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource, TimelineOrigin origin});
|
typedef TimelineQuery = ({TimelineAssetSource assetSource, TimelineBucketSource bucketSource, TimelineOrigin origin});
|
||||||
|
|
||||||
|
enum TimelineStatus { uninitialized, ready, disposed }
|
||||||
|
|
||||||
enum TimelineOrigin {
|
enum TimelineOrigin {
|
||||||
main,
|
main,
|
||||||
localAlbum,
|
localAlbum,
|
||||||
@@ -101,9 +103,13 @@ class TimelineService {
|
|||||||
int _bufferOffset = 0;
|
int _bufferOffset = 0;
|
||||||
List<BaseAsset> _buffer = [];
|
List<BaseAsset> _buffer = [];
|
||||||
StreamSubscription? _bucketSubscription;
|
StreamSubscription? _bucketSubscription;
|
||||||
|
final StreamController<TimelineStatus> _statusController = StreamController<TimelineStatus>.broadcast();
|
||||||
|
|
||||||
int _totalAssets = 0;
|
int _totalAssets = 0;
|
||||||
int get totalAssets => _totalAssets;
|
int get totalAssets => _totalAssets;
|
||||||
|
TimelineStatus _status = TimelineStatus.uninitialized;
|
||||||
|
TimelineStatus get status => _status;
|
||||||
|
bool get isReady => _status == TimelineStatus.ready;
|
||||||
|
|
||||||
TimelineService(TimelineQuery query)
|
TimelineService(TimelineQuery query)
|
||||||
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
|
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
|
||||||
@@ -139,12 +145,17 @@ class TimelineService {
|
|||||||
|
|
||||||
// change the state's total assets count only after the buffer is reloaded
|
// change the state's total assets count only after the buffer is reloaded
|
||||||
_totalAssets = totalAssets;
|
_totalAssets = totalAssets;
|
||||||
|
if (_status == TimelineStatus.uninitialized) {
|
||||||
|
_status = TimelineStatus.ready;
|
||||||
|
_statusController.add(_status);
|
||||||
|
}
|
||||||
EventStream.shared.emit(const TimelineReloadEvent());
|
EventStream.shared.emit(const TimelineReloadEvent());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
Stream<TimelineStatus> watchStatus() => _statusController.stream;
|
||||||
|
|
||||||
Future<List<BaseAsset>> loadAssets(int index, int count) => _mutex.run(() => _loadAssets(index, count));
|
Future<List<BaseAsset>> loadAssets(int index, int count) => _mutex.run(() => _loadAssets(index, count));
|
||||||
|
|
||||||
@@ -247,5 +258,12 @@ class TimelineService {
|
|||||||
_bucketSubscription = null;
|
_bucketSubscription = null;
|
||||||
_buffer = [];
|
_buffer = [];
|
||||||
_bufferOffset = 0;
|
_bufferOffset = 0;
|
||||||
|
if (_status != TimelineStatus.disposed) {
|
||||||
|
_status = TimelineStatus.disposed;
|
||||||
|
if (!_statusController.isClosed) {
|
||||||
|
_statusController.add(_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _statusController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ extension StringExtension on String {
|
|||||||
String capitalize() {
|
String capitalize() {
|
||||||
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
|
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get nullIfEmpty => isEmpty ? null : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DurationExtension on String {
|
extension DurationExtension on String {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
@@ -139,9 +140,38 @@ extension<T extends Object> on MetadataDomain<T> {
|
|||||||
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
|
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
|
||||||
tapToNavigate: repo._read(.viewerTapToNavigate),
|
tapToNavigate: repo._read(.viewerTapToNavigate),
|
||||||
),
|
),
|
||||||
|
slideshow: .new(
|
||||||
|
transition: repo._read(.slideshowTransition),
|
||||||
|
repeat: repo._read(.slideshowRepeat),
|
||||||
|
duration: repo._read(.slideshowDuration),
|
||||||
|
look: repo._read(.slideshowLook),
|
||||||
|
direction: repo._read(.slideshowDirection),
|
||||||
|
),
|
||||||
|
album: .new(
|
||||||
|
sortMode: repo._read(.albumSortMode),
|
||||||
|
isReverse: repo._read(.albumIsReverse),
|
||||||
|
isGrid: repo._read(.albumIsGrid),
|
||||||
|
),
|
||||||
|
backup: .new(
|
||||||
|
enabled: repo._read(.backupEnabled),
|
||||||
|
useCellularForVideos: repo._read(.backupUseCellularForVideos),
|
||||||
|
useCellularForPhotos: repo._read(.backupUseCellularForPhotos),
|
||||||
|
requireCharging: repo._read(.backupRequireCharging),
|
||||||
|
triggerDelay: repo._read(.backupTriggerDelay),
|
||||||
|
syncAlbums: repo._read(.backupSyncAlbums),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case .systemConfig:
|
case .systemConfig:
|
||||||
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
repo._systemConfig = .new(
|
||||||
|
logLevel: repo._read(.logLevel),
|
||||||
|
network: .new(
|
||||||
|
autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching),
|
||||||
|
preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty,
|
||||||
|
localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty,
|
||||||
|
externalEndpointList: repo._read(.networkExternalEndpointList),
|
||||||
|
customHeaders: repo._read(.networkCustomHeaders),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
enum SortRemoteAlbumsBy { id, updatedAt }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
@@ -159,7 +160,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
createdAt: Value(album.createdAt),
|
createdAt: Value(album.createdAt),
|
||||||
updatedAt: Value(album.updatedAt),
|
updatedAt: Value(album.updatedAt),
|
||||||
description: Value(album.description),
|
description: Value(album.description),
|
||||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
thumbnailAssetId: Value(album.thumbnailAssetId ?? (assetIds.isNotEmpty ? assetIds.first : null)),
|
||||||
isActivityEnabled: Value(album.isActivityEnabled),
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
order: Value(album.order),
|
order: Value(album.order),
|
||||||
);
|
);
|
||||||
@@ -274,17 +275,59 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
||||||
|
if (assetIds.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
final albumAssets = assetIds.map(
|
final albumAssets = assetIds.map(
|
||||||
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(albumId), assetId: Value(assetId)),
|
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(albumId), assetId: Value(assetId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _db.batch((batch) {
|
await _db.transaction(() async {
|
||||||
batch.insertAll(_db.remoteAlbumAssetEntity, albumAssets);
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(_db.remoteAlbumAssetEntity, albumAssets);
|
||||||
|
});
|
||||||
|
|
||||||
|
final album = _db.update(_db.remoteAlbumEntity)
|
||||||
|
..where((row) => row.id.equals(albumId) & row.thumbnailAssetId.isNull());
|
||||||
|
|
||||||
|
await album.write(RemoteAlbumEntityCompanion(thumbnailAssetId: Value(assetIds.first)));
|
||||||
});
|
});
|
||||||
|
|
||||||
return assetIds.length;
|
return assetIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts a placeholder `remote_asset_entity` row from a freshly-uploaded
|
||||||
|
/// local asset. Skips silently if a row with the same id or
|
||||||
|
/// (owner_id, checksum) already exists — sync will overwrite with the
|
||||||
|
/// authoritative server data once the AssetUploadReadyV1 event is processed.
|
||||||
|
Future<void> upsertRemoteAssetStub({
|
||||||
|
required String remoteId,
|
||||||
|
required String ownerId,
|
||||||
|
required LocalAsset source,
|
||||||
|
}) async {
|
||||||
|
await _db
|
||||||
|
.into(_db.remoteAssetEntity)
|
||||||
|
.insert(
|
||||||
|
RemoteAssetEntityCompanion(
|
||||||
|
id: Value(remoteId),
|
||||||
|
ownerId: Value(ownerId),
|
||||||
|
checksum: Value(source.checksum ?? remoteId),
|
||||||
|
name: Value(source.name),
|
||||||
|
type: Value(source.type),
|
||||||
|
createdAt: Value(source.createdAt),
|
||||||
|
updatedAt: Value(source.updatedAt),
|
||||||
|
width: Value(source.width),
|
||||||
|
height: Value(source.height),
|
||||||
|
durationMs: Value(source.durationMs),
|
||||||
|
isFavorite: Value(source.isFavorite),
|
||||||
|
visibility: const Value(AssetVisibility.timeline),
|
||||||
|
isEdited: Value(source.isEdited),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addUsers(String albumId, List<String> userIds) {
|
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||||
final albumUsers = userIds.map(
|
final albumUsers = userIds.map(
|
||||||
(assetId) => RemoteAlbumUserEntityCompanion(
|
(assetId) => RemoteAlbumUserEntityCompanion(
|
||||||
|
|||||||
@@ -14,4 +14,13 @@ class TagsApiRepository extends ApiRepository {
|
|||||||
Future<List<TagResponseDto>?> getAllTags() async {
|
Future<List<TagResponseDto>?> getAllTags() async {
|
||||||
return await _api.getAllTags();
|
return await _api.getAllTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||||
|
final response = await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
|
||||||
|
return response?.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TagResponseDto>?> upsertTags(List<String> tags) async {
|
||||||
|
return _api.upsertTags(TagUpsertDto(tags: tags));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Bucket> _generateBuckets(int count) {
|
List<Bucket> _generateBuckets(int count) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
|||||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
@@ -128,6 +129,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
dPrint(() => "[APP STATE] resumed");
|
dPrint(() => "[APP STATE] resumed");
|
||||||
ref.read(appStateProvider.notifier).handleAppResume();
|
ref.read(appStateProvider.notifier).handleAppResume();
|
||||||
|
unawaited(ref.read(viewIntentHandlerProvider).onAppResumed());
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
dPrint(() => "[APP STATE] inactive");
|
dPrint(() => "[APP STATE] inactive");
|
||||||
@@ -233,6 +235,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.read(viewIntentHandlerProvider).init();
|
||||||
ref.read(shareIntentUploadProvider.notifier).init();
|
ref.read(shareIntentUploadProvider.notifier).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
extension ViewIntentPayloadX on ViewIntentPayload {
|
||||||
|
String get fileName {
|
||||||
|
final resolvedPath = path;
|
||||||
|
if (resolvedPath != null && resolvedPath.isNotEmpty) {
|
||||||
|
return basename(resolvedPath);
|
||||||
|
}
|
||||||
|
return localAssetId ?? 'view_intent_asset';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isImage => mimeType.toLowerCase().startsWith('image/');
|
||||||
|
|
||||||
|
bool get isVideo => mimeType.toLowerCase().startsWith('video/');
|
||||||
|
|
||||||
|
AssetPlaybackStyle get playbackStyle {
|
||||||
|
if (isVideo) {
|
||||||
|
return AssetPlaybackStyle.video;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedMimeType = mimeType.toLowerCase();
|
||||||
|
if (normalizedMimeType == 'image/gif' || normalizedMimeType == 'image/webp') {
|
||||||
|
return AssetPlaybackStyle.imageAnimated;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedPath = path?.toLowerCase();
|
||||||
|
if (normalizedPath != null && (normalizedPath.endsWith('.gif') || normalizedPath.endsWith('.webp'))) {
|
||||||
|
return AssetPlaybackStyle.imageAnimated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AssetPlaybackStyle.image;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
_searchController = TextEditingController();
|
_searchController = TextEditingController();
|
||||||
_searchFocusNode = FocusNode();
|
_searchFocusNode = FocusNode();
|
||||||
|
|
||||||
_enableSyncUploadAlbum.value = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
_enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||||
ref.read(backupAlbumProvider.notifier).getAll();
|
ref.read(backupAlbumProvider.notifier).getAll();
|
||||||
|
|
||||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
||||||
final selectedAlbums = ref
|
final selectedAlbums = ref
|
||||||
.read(backupAlbumProvider)
|
.read(backupAlbumProvider)
|
||||||
.where((a) => a.backupSelection == BackupSelection.selected)
|
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||||
@@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
|
||||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import 'dart:async';
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
|
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@@ -21,18 +19,20 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
bool hasPopped = false;
|
bool hasPopped = false;
|
||||||
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
|
final previousBackup = ref.read(metadataProvider).appConfig.backup;
|
||||||
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
|
final previousCellularForVideos = previousBackup.useCellularForVideos;
|
||||||
|
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
// There is an issue with Flutter where the pop event
|
// There is an issue with Flutter where the pop event
|
||||||
// can be triggered multiple times, so we guard it with _hasPopped
|
// can be triggered multiple times, so we guard it with _hasPopped
|
||||||
|
|
||||||
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
|
final currentBackup = ref.read(metadataProvider).appConfig.backup;
|
||||||
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
|
final currentCellularForVideos = currentBackup.useCellularForVideos;
|
||||||
|
final currentCellularForPhotos = currentBackup.useCellularForPhotos;
|
||||||
|
|
||||||
if (currentWifiReqForVideos == previousWifiReqForVideos &&
|
if (currentCellularForVideos == previousCellularForVideos &&
|
||||||
currentWifiReqForPhotos == previousWifiReqForPhotos) {
|
currentCellularForPhotos == previousCellularForPhotos) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
|
||||||
if (!isBackupEnabled) {
|
if (!isBackupEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
|
||||||
class SettingsHeader {
|
class SettingsHeader {
|
||||||
String key = "";
|
String key = "";
|
||||||
@@ -24,17 +22,14 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
final headers = useState<List<SettingsHeader>>([]);
|
final headers = useState<List<SettingsHeader>>([]);
|
||||||
final setInitialHeaders = useState(false);
|
final setInitialHeaders = useState(false);
|
||||||
|
|
||||||
var headersStr = Store.get(StoreKey.customHeaders, "");
|
final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
|
||||||
if (!setInitialHeaders.value) {
|
if (!setInitialHeaders.value) {
|
||||||
if (headersStr.isNotEmpty) {
|
storedHeaders.forEach((k, v) {
|
||||||
var customHeaders = jsonDecode(headersStr) as Map;
|
final header = SettingsHeader();
|
||||||
customHeaders.forEach((k, v) {
|
header.key = k;
|
||||||
final header = SettingsHeader();
|
header.value = v;
|
||||||
header.key = k;
|
headers.value.add(header);
|
||||||
header.value = v;
|
});
|
||||||
headers.value.add(header);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// add first one to help the user
|
// add first one to help the user
|
||||||
if (headers.value.isEmpty) {
|
if (headers.value.isEmpty) {
|
||||||
@@ -88,8 +83,8 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
|
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
|
||||||
final headersMap = {};
|
final headersMap = <String, String>{};
|
||||||
for (var header in headers) {
|
for (final header in headers) {
|
||||||
final key = header.key.trim();
|
final key = header.key.trim();
|
||||||
final value = header.value.trim();
|
final value = header.value.trim();
|
||||||
|
|
||||||
@@ -99,8 +94,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
headersMap[key] = value;
|
headersMap[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
var encoded = jsonEncode(headersMap);
|
await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap);
|
||||||
await Store.put(StoreKey.customHeaders, encoded);
|
|
||||||
await ref.read(apiServiceProvider).updateHeaders();
|
await ref.read(apiServiceProvider).updateHeaders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||||
@@ -313,6 +315,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
final wsProvider = ref.read(websocketProvider.notifier);
|
final wsProvider = ref.read(websocketProvider.notifier);
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||||
|
final viewIntentHandler = ref.read(viewIntentHandlerProvider);
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||||
@@ -327,6 +330,8 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await viewIntentHandler.flushDeferredViewIntent();
|
||||||
|
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
backgroundManager.hashAssets().then((_) {
|
backgroundManager.hashAssets().then((_) {
|
||||||
@@ -340,7 +345,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
await backgroundManager.hashAssets();
|
await backgroundManager.hashAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
|
||||||
await backgroundManager.syncLinkedAlbum();
|
await backgroundManager.syncLinkedAlbum();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -369,7 +374,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
|
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
|
||||||
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
|
final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled;
|
||||||
|
|
||||||
if (isEnableBackup) {
|
if (isEnableBackup) {
|
||||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart';
|
import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart';
|
||||||
@@ -28,71 +27,41 @@ class SharedLinkPage extends HookConsumerWidget {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
Widget buildNoShares() {
|
Widget buildNoShares() {
|
||||||
return Column(
|
return Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Padding(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
children: [
|
||||||
child: const Text(
|
Icon(Icons.link_off, size: 100, color: Theme.of(context).colorScheme.onSurface.withAlpha(128)),
|
||||||
"shared_link_manage_links",
|
const SizedBox(height: 20),
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
|
const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(),
|
||||||
).tr(),
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
child: const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.link_off, size: 100, color: context.themeData.iconTheme.color?.withValues(alpha: 0.5)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSharesList(List<SharedLink> links) {
|
Widget buildSharesList(List<SharedLink> links) {
|
||||||
return Column(
|
return LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, constraints) => constraints.maxWidth > 600
|
||||||
children: [
|
? GridView.builder(
|
||||||
Padding(
|
key: const PageStorageKey('shared-links-grid'),
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0),
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
child: Text(
|
crossAxisCount: 2,
|
||||||
"shared_link_manage_links",
|
mainAxisExtent: 180,
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.textTheme.labelLarge?.color?.withAlpha(200)),
|
crossAxisSpacing: 12,
|
||||||
).tr(),
|
mainAxisSpacing: 12,
|
||||||
),
|
),
|
||||||
Expanded(
|
padding: const EdgeInsets.all(12),
|
||||||
child: LayoutBuilder(
|
itemCount: links.length,
|
||||||
builder: (context, constraints) {
|
itemBuilder: (context, index) => SharedLinkItem(links[index]),
|
||||||
if (constraints.maxWidth > 600) {
|
)
|
||||||
// Two column
|
: ListView.separated(
|
||||||
return GridView.builder(
|
key: const PageStorageKey('shared-links-list'),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
crossAxisCount: 2,
|
itemCount: links.length,
|
||||||
mainAxisExtent: 180,
|
itemBuilder: (context, index) => SharedLinkItem(links[index]),
|
||||||
),
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
itemCount: links.length,
|
),
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return SharedLinkItem(links.elementAt(index));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single column
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: links.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return SharedLinkItem(links.elementAt(index));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/services/shared_link.service.dart';
|
import 'package:immich_mobile/services/shared_link.service.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SharedLinkEditPage extends HookConsumerWidget {
|
class SharedLinkEditPage extends HookConsumerWidget {
|
||||||
|
static const int maxFutureDate = 365 * 2;
|
||||||
|
|
||||||
final SharedLink? existingLink;
|
final SharedLink? existingLink;
|
||||||
final List<String>? assetsList;
|
final List<String>? assetsList;
|
||||||
final String? albumId;
|
final String? albumId;
|
||||||
@@ -23,71 +28,82 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const padding = 20.0;
|
|
||||||
final themeData = context.themeData;
|
final themeData = context.themeData;
|
||||||
final colorScheme = context.colorScheme;
|
final colorScheme = context.colorScheme;
|
||||||
|
final externalDomain = ref.watch(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
final displayServerUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
|
final expiryPresets = <(Duration, String)>[
|
||||||
|
(Duration.zero, context.t.never),
|
||||||
|
(const Duration(minutes: 30), context.t.shared_link_edit_expire_after_option_minutes(count: 30)),
|
||||||
|
(const Duration(hours: 1), context.t.shared_link_edit_expire_after_option_hour),
|
||||||
|
(const Duration(hours: 6), context.t.shared_link_edit_expire_after_option_hours(count: 6)),
|
||||||
|
(const Duration(days: 1), context.t.shared_link_edit_expire_after_option_day),
|
||||||
|
(const Duration(days: 7), context.t.shared_link_edit_expire_after_option_days(count: 7)),
|
||||||
|
(const Duration(days: 30), context.t.shared_link_edit_expire_after_option_days(count: 30)),
|
||||||
|
(const Duration(days: 90), context.t.shared_link_edit_expire_after_option_months(count: 3)),
|
||||||
|
(const Duration(days: 365), context.t.shared_link_edit_expire_after_option_year(count: 1)),
|
||||||
|
];
|
||||||
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
||||||
final descriptionFocusNode = useFocusNode();
|
final descriptionFocusNode = useFocusNode();
|
||||||
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
||||||
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
||||||
final slugFocusNode = useFocusNode();
|
final slugFocusNode = useFocusNode();
|
||||||
|
useListenable(slugController);
|
||||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||||
final editExpiry = useState(false);
|
final expiryAfter = useState<DateTime?>(existingLink?.expiresAt?.toLocal());
|
||||||
final expiryAfter = useState(0);
|
final selectedPresetIndex = useState<int?>(existingLink?.expiresAt == null ? 0 : null);
|
||||||
final newShareLink = useState("");
|
final newShareLink = useState("");
|
||||||
|
|
||||||
|
Widget buildSharedLinkRow({required String leading, required String content}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(leading, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildLinkTitle() {
|
Widget buildLinkTitle() {
|
||||||
if (existingLink != null) {
|
if (existingLink != null) {
|
||||||
if (existingLink!.type == SharedLinkSource.album) {
|
if (existingLink!.type == SharedLinkSource.album) {
|
||||||
return Row(
|
return buildSharedLinkRow(leading: context.t.public_album, content: existingLink!.title);
|
||||||
children: [
|
|
||||||
const Text('public_album', style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
|
||||||
const Text(" | ", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
Text(
|
|
||||||
existingLink!.title,
|
|
||||||
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingLink!.type == SharedLinkSource.individual) {
|
if (existingLink!.type == SharedLinkSource.individual) {
|
||||||
return Row(
|
return buildSharedLinkRow(
|
||||||
children: [
|
leading: context.t.shared_link_individual_shared,
|
||||||
const Text('shared_link_individual_shared', style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
content: existingLink!.description ?? "--",
|
||||||
const Text(" | ", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
existingLink!.description ?? "--",
|
|
||||||
style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.bold),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return const Text("create_link_to_share_description", style: TextStyle(fontWeight: FontWeight.bold)).tr();
|
return Text(context.t.create_link_to_share_description, style: const TextStyle(fontWeight: FontWeight.bold));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildDescriptionField() {
|
Widget buildDescriptionField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: descriptionController,
|
controller: descriptionController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
focusNode: descriptionFocusNode,
|
focusNode: descriptionFocusNode,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'description'.tr(),
|
labelText: context.t.description,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'shared_link_edit_description_hint'.tr(),
|
hintText: context.t.shared_link_edit_description_hint,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
onTapOutside: (_) => descriptionFocusNode.unfocus(),
|
||||||
);
|
);
|
||||||
@@ -96,16 +112,14 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildPasswordField() {
|
Widget buildPasswordField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'password'.tr(),
|
labelText: context.t.password,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'shared_link_edit_password_hint'.tr(),
|
hintText: context.t.shared_link_edit_password_hint,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,18 +127,16 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildSlugField() {
|
Widget buildSlugField() {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: slugController,
|
controller: slugController,
|
||||||
enabled: newShareLink.value.isEmpty,
|
|
||||||
focusNode: slugFocusNode,
|
focusNode: slugFocusNode,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'custom_url'.tr(),
|
labelText: slugController.text.isNotEmpty ? context.t.custom_url : null,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'custom_url'.tr(),
|
hintText: context.t.custom_url,
|
||||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
prefixText: slugController.text.isNotEmpty ? '/s/' : null,
|
||||||
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
prefixStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => slugFocusNode.unfocus(),
|
onTapOutside: (_) => slugFocusNode.unfocus(),
|
||||||
);
|
);
|
||||||
@@ -133,145 +145,182 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Widget buildShowMetaButton() {
|
Widget buildShowMetaButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: showMetadata.value,
|
value: showMetadata.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null,
|
onChanged: (value) => showMetadata.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text("show_metadata", style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(),
|
title: Text(
|
||||||
|
context.t.show_metadata,
|
||||||
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildAllowDownloadButton() {
|
Widget buildAllowDownloadButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: allowDownload.value,
|
value: allowDownload.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null,
|
onChanged: (value) => allowDownload.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
"allow_public_user_to_download",
|
context.t.allow_public_user_to_download,
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildAllowUploadButton() {
|
Widget buildAllowUploadButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: allowUpload.value,
|
value: allowUpload.value,
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null,
|
onChanged: (value) => allowUpload.value = value,
|
||||||
activeThumbColor: colorScheme.primary,
|
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
"allow_public_user_to_upload",
|
context.t.allow_public_user_to_upload,
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildEditExpiryButton() {
|
String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(context.locale.toString()).add_Hm().format(dateTime);
|
||||||
return SwitchListTile.adaptive(
|
|
||||||
value: editExpiry.value,
|
DateTime? getExpiresAtFromPreset(Duration preset) => preset == Duration.zero ? null : DateTime.now().add(preset);
|
||||||
onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null,
|
|
||||||
activeThumbColor: colorScheme.primary,
|
Future<void> selectDate() async {
|
||||||
dense: true,
|
final today = DateTime.now();
|
||||||
title: Text(
|
final safeInitialDate = expiryAfter.value ?? today.add(const Duration(days: 7));
|
||||||
"change_expiration_time",
|
final initialDate = safeInitialDate.isBefore(today) ? today : safeInitialDate;
|
||||||
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
final selectedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: today,
|
||||||
|
lastDate: today.add(const Duration(days: maxFutureDate)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (selectedDate != null && context.mounted) {
|
||||||
|
final isToday =
|
||||||
|
selectedDate.year == today.year && selectedDate.month == today.month && selectedDate.day == today.day;
|
||||||
|
final initialTime = isToday ? TimeOfDay.fromDateTime(today) : const TimeOfDay(hour: 12, minute: 0);
|
||||||
|
|
||||||
|
final selectedTime = await showTimePicker(context: context, initialTime: initialTime);
|
||||||
|
|
||||||
|
if (selectedTime != null) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
var finalDateTime = DateTime(
|
||||||
|
selectedDate.year,
|
||||||
|
selectedDate.month,
|
||||||
|
selectedDate.day,
|
||||||
|
selectedTime.hour,
|
||||||
|
selectedTime.minute,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalDateTime.isBefore(now) && isToday) {
|
||||||
|
finalDateTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPresetIndex.value = null;
|
||||||
|
expiryAfter.value = finalDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildExpiryAfterButton() {
|
Widget buildExpiryAfterButton() {
|
||||||
return DropdownMenu(
|
return ExpansionTile(
|
||||||
label: Text(
|
title: Text(
|
||||||
"expire_after",
|
context.t.expire_after,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
),
|
||||||
enableSearch: false,
|
subtitle: Text(
|
||||||
enableFilter: false,
|
expiryAfter.value == null ? context.t.shared_link_expires_never : formatDateTime(expiryAfter.value!),
|
||||||
width: context.width - 40,
|
style: TextStyle(color: themeData.colorScheme.primary),
|
||||||
initialSelection: expiryAfter.value,
|
),
|
||||||
enabled: newShareLink.value.isEmpty && (existingLink == null || editExpiry.value),
|
|
||||||
onSelected: (value) {
|
|
||||||
expiryAfter.value = value!;
|
|
||||||
},
|
|
||||||
dropdownMenuEntries: [
|
|
||||||
DropdownMenuEntry(value: 0, label: "never".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 30,
|
|
||||||
label: "shared_link_edit_expire_after_option_minutes".tr(namedArgs: {'count': "30"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(value: 60, label: "shared_link_edit_expire_after_option_hour".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 6,
|
|
||||||
label: "shared_link_edit_expire_after_option_hours".tr(namedArgs: {'count': "6"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(value: 60 * 24, label: "shared_link_edit_expire_after_option_day".tr()),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 7,
|
|
||||||
label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "7"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30,
|
|
||||||
label: "shared_link_edit_expire_after_option_days".tr(namedArgs: {'count': "30"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30 * 3,
|
|
||||||
label: "shared_link_edit_expire_after_option_months".tr(namedArgs: {'count': "3"}),
|
|
||||||
),
|
|
||||||
DropdownMenuEntry(
|
|
||||||
value: 60 * 24 * 30 * 12,
|
|
||||||
label: "shared_link_edit_expire_after_option_year".tr(namedArgs: {'count': "1"}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void copyLinkToClipboard() {
|
|
||||||
Clipboard.setData(ClipboardData(text: newShareLink.value)).then((_) {
|
|
||||||
context.scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
"shared_link_clipboard_copied_massage",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
|
||||||
).tr(),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNewLinkField() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
const Padding(padding: EdgeInsets.only(top: 20, bottom: 20), child: Divider()),
|
|
||||||
TextFormField(
|
|
||||||
readOnly: true,
|
|
||||||
initialValue: newShareLink.value,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
|
||||||
suffixIcon: IconButton(onPressed: copyLinkToClipboard, icon: const Icon(Icons.copy)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: Align(
|
child: Column(
|
||||||
alignment: Alignment.bottomRight,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: ElevatedButton(
|
children: [
|
||||||
onPressed: () {
|
Wrap(
|
||||||
context.maybePop();
|
spacing: 8,
|
||||||
},
|
runSpacing: 8,
|
||||||
child: const Text("done", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
|
children: List.generate(expiryPresets.length, (index) {
|
||||||
),
|
final preset = expiryPresets[index];
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(preset.$2),
|
||||||
|
selected: selectedPresetIndex.value == index,
|
||||||
|
onSelected: (_) {
|
||||||
|
selectedPresetIndex.value = index;
|
||||||
|
expiryAfter.value = getExpiresAtFromPreset(preset.$1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
if (expiryAfter.value != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: selectDate,
|
||||||
|
icon: const Icon(Icons.edit_calendar),
|
||||||
|
label: Text(context.t.edit_date_and_time),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime calculateExpiry() {
|
Future<void> copyToClipboard(String link) async {
|
||||||
return DateTime.now().add(Duration(minutes: expiryAfter.value));
|
await Clipboard.setData(ClipboardData(text: link));
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.t.shared_link_clipboard_copied_massage,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildLinkCopyField(String link) {
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => copyToClipboard(link),
|
||||||
|
initialValue: link,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
enabledBorder: themeData.inputDecorationTheme.focusedBorder,
|
||||||
|
suffixIcon: IconButton(onPressed: () => Share.share(link), icon: const Icon(Icons.share)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNewLinkReadyScreen() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add_link, size: 100, color: themeData.colorScheme.primary),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
buildLinkCopyField(newShareLink.value),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => context.maybePop(),
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(context.t.done, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? calculateExpiry() => expiryAfter.value;
|
||||||
|
|
||||||
Future<void> handleNewLink() async {
|
Future<void> handleNewLink() async {
|
||||||
final newLink = await ref
|
final newLink = await ref
|
||||||
.read(sharedLinkServiceProvider)
|
.read(sharedLinkServiceProvider)
|
||||||
@@ -284,30 +333,30 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
||||||
password: passwordController.text.isEmpty ? null : passwordController.text,
|
password: passwordController.text.isEmpty ? null : passwordController.text,
|
||||||
slug: slugController.text.isEmpty ? null : slugController.text,
|
slug: slugController.text.isEmpty ? null : slugController.text,
|
||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: calculateExpiry()?.toUtc(),
|
||||||
);
|
);
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
|
||||||
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
|
||||||
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
final serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
|
||||||
serverUrl += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null) {
|
||||||
final hasSlug = newLink.slug?.isNotEmpty == true;
|
newShareLink.value = buildSharedLinkUrl(baseUrl: serverUrl, slug: newLink.slug, key: newLink.key) ?? '';
|
||||||
final urlPath = hasSlug ? newLink.slug : newLink.key;
|
await copyToClipboard(newShareLink.value);
|
||||||
final basePath = hasSlug ? 's' : 'share';
|
} else {
|
||||||
newShareLink.value = "$serverUrl$basePath/$urlPath";
|
|
||||||
copyLinkToClipboard();
|
|
||||||
} else if (newLink == null) {
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
msg: 'shared_link_create_error'.tr(),
|
msg: context.t.shared_link_create_error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,8 +397,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
slug = existingLink!.slug;
|
slug = existingLink!.slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editExpiry.value) {
|
final newExpiry = expiryAfter.value;
|
||||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
if (newExpiry?.toUtc() != existingLink!.expiresAt?.toUtc()) {
|
||||||
|
expiry = newExpiry;
|
||||||
changeExpiry = true;
|
changeExpiry = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,69 +413,115 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
description: desc,
|
description: desc,
|
||||||
password: password,
|
password: password,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry?.toUtc(),
|
||||||
changeExpiry: changeExpiry,
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
await context.maybePop();
|
await context.maybePop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleDeleteLink() async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => ConfirmDialog(
|
||||||
|
title: "delete_shared_link_dialog_title",
|
||||||
|
content: "confirm_delete_shared_link",
|
||||||
|
onOk: () async {
|
||||||
|
await ref.read(sharedLinkServiceProvider).deleteSharedLink(existingLink!.id);
|
||||||
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
await context.maybePop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(existingLink == null ? "create_link_to_share" : "edit_link").tr(),
|
title: Text(existingLink == null ? context.t.create_link_to_share : context.t.edit_link),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: const CloseButton(),
|
leading: const CloseButton(),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListView(
|
child: newShareLink.value.isEmpty
|
||||||
children: [
|
? Padding(
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
child: ListView(
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
children: [
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
|
const SizedBox(height: 20),
|
||||||
Padding(
|
buildLinkTitle(),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
if (existingLink != null)
|
||||||
child: buildShowMetaButton(),
|
Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 16),
|
||||||
child: buildAllowDownloadButton(),
|
buildLinkCopyField(
|
||||||
),
|
buildSharedLinkUrl(
|
||||||
Padding(
|
baseUrl: displayServerUrl,
|
||||||
padding: const EdgeInsets.only(left: padding, right: 20, bottom: 20),
|
slug: existingLink!.slug,
|
||||||
child: buildAllowUploadButton(),
|
key: existingLink!.key,
|
||||||
),
|
) ??
|
||||||
if (existingLink != null)
|
'',
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 24),
|
||||||
child: buildEditExpiryButton(),
|
const Divider(),
|
||||||
),
|
],
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
const SizedBox(height: 24),
|
||||||
child: buildExpiryAfterButton(),
|
buildDescriptionField(),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
if (newShareLink.value.isEmpty)
|
buildPasswordField(),
|
||||||
Align(
|
const SizedBox(height: 16),
|
||||||
alignment: Alignment.bottomRight,
|
buildSlugField(),
|
||||||
child: Padding(
|
const SizedBox(height: 16),
|
||||||
padding: const EdgeInsets.only(right: padding + 10, bottom: padding),
|
buildShowMetaButton(),
|
||||||
child: ElevatedButton(
|
const SizedBox(height: 16),
|
||||||
onPressed: existingLink != null ? handleEditLink : handleNewLink,
|
buildAllowDownloadButton(),
|
||||||
child: Text(
|
const SizedBox(height: 16),
|
||||||
existingLink != null ? "shared_link_edit_submit_button" : "create_link",
|
buildAllowUploadButton(),
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
const SizedBox(height: 16),
|
||||||
).tr(),
|
buildExpiryAfterButton(),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
if (existingLink != null)
|
||||||
|
OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: themeData.colorScheme.error,
|
||||||
|
side: BorderSide(color: themeData.colorScheme.error),
|
||||||
|
),
|
||||||
|
onPressed: handleDeleteLink,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: Text(
|
||||||
|
context.t.delete,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
onPressed: existingLink != null ? handleEditLink : handleNewLink,
|
||||||
|
label: Text(
|
||||||
|
existingLink != null ? context.t.shared_link_edit_submit_button : context.t.create_link,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
if (newShareLink.value.isNotEmpty)
|
: Center(child: buildNewLinkReadyScreen()),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
|
||||||
child: buildNewLinkField(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+183
-120
@@ -9,14 +9,22 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||||
|
|
||||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
Object? _extractReplyValueOrThrow(
|
||||||
|
List<Object?>? replyList,
|
||||||
|
String channelName, {
|
||||||
|
required bool isNullValid,
|
||||||
|
}) {
|
||||||
if (replyList == null) {
|
if (replyList == null) {
|
||||||
throw PlatformException(
|
throw PlatformException(
|
||||||
code: 'channel-error',
|
code: 'channel-error',
|
||||||
message: 'Unable to establish connection on channel: "$channelName".',
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
);
|
);
|
||||||
} else if (replyList.length > 1) {
|
} else if (replyList.length > 1) {
|
||||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
throw PlatformException(
|
||||||
|
code: replyList[0]! as String,
|
||||||
|
message: replyList[1] as String?,
|
||||||
|
details: replyList[2],
|
||||||
|
);
|
||||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||||
throw PlatformException(
|
throw PlatformException(
|
||||||
code: 'null-error',
|
code: 'null-error',
|
||||||
@@ -37,7 +45,9 @@ bool _deepEquals(Object? a, Object? b) {
|
|||||||
return a == b;
|
return a == b;
|
||||||
}
|
}
|
||||||
if (a is List && b is List) {
|
if (a is List && b is List) {
|
||||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
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 is Map && b is Map) {
|
||||||
if (a.length != b.length) {
|
if (a.length != b.length) {
|
||||||
@@ -86,7 +96,15 @@ int _deepHash(Object? value) {
|
|||||||
return value.hashCode;
|
return value.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
|
|
||||||
|
enum PlatformAssetPlaybackStyle {
|
||||||
|
unknown,
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
imageAnimated,
|
||||||
|
livePhoto,
|
||||||
|
videoLooping,
|
||||||
|
}
|
||||||
|
|
||||||
class PlatformAsset {
|
class PlatformAsset {
|
||||||
PlatformAsset({
|
PlatformAsset({
|
||||||
@@ -154,8 +172,7 @@ class PlatformAsset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static PlatformAsset decode(Object result) {
|
static PlatformAsset decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
@@ -186,20 +203,7 @@ class PlatformAsset {
|
|||||||
if (identical(this, other)) {
|
if (identical(this, other)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _deepEquals(id, other.id) &&
|
return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(type, other.type) && _deepEquals(createdAt, other.createdAt) && _deepEquals(updatedAt, other.updatedAt) && _deepEquals(width, other.width) && _deepEquals(height, other.height) && _deepEquals(durationMs, other.durationMs) && _deepEquals(orientation, other.orientation) && _deepEquals(isFavorite, other.isFavorite) && _deepEquals(adjustmentTime, other.adjustmentTime) && _deepEquals(latitude, other.latitude) && _deepEquals(longitude, other.longitude) && _deepEquals(playbackStyle, other.playbackStyle);
|
||||||
_deepEquals(name, other.name) &&
|
|
||||||
_deepEquals(type, other.type) &&
|
|
||||||
_deepEquals(createdAt, other.createdAt) &&
|
|
||||||
_deepEquals(updatedAt, other.updatedAt) &&
|
|
||||||
_deepEquals(width, other.width) &&
|
|
||||||
_deepEquals(height, other.height) &&
|
|
||||||
_deepEquals(durationMs, other.durationMs) &&
|
|
||||||
_deepEquals(orientation, other.orientation) &&
|
|
||||||
_deepEquals(isFavorite, other.isFavorite) &&
|
|
||||||
_deepEquals(adjustmentTime, other.adjustmentTime) &&
|
|
||||||
_deepEquals(latitude, other.latitude) &&
|
|
||||||
_deepEquals(longitude, other.longitude) &&
|
|
||||||
_deepEquals(playbackStyle, other.playbackStyle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -227,12 +231,17 @@ class PlatformAlbum {
|
|||||||
int assetCount;
|
int assetCount;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[id, name, updatedAt, isCloud, assetCount];
|
return <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
updatedAt,
|
||||||
|
isCloud,
|
||||||
|
assetCount,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static PlatformAlbum decode(Object result) {
|
static PlatformAlbum decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
@@ -254,11 +263,7 @@ class PlatformAlbum {
|
|||||||
if (identical(this, other)) {
|
if (identical(this, other)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _deepEquals(id, other.id) &&
|
return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(updatedAt, other.updatedAt) && _deepEquals(isCloud, other.isCloud) && _deepEquals(assetCount, other.assetCount);
|
||||||
_deepEquals(name, other.name) &&
|
|
||||||
_deepEquals(updatedAt, other.updatedAt) &&
|
|
||||||
_deepEquals(isCloud, other.isCloud) &&
|
|
||||||
_deepEquals(assetCount, other.assetCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -267,7 +272,12 @@ class PlatformAlbum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SyncDelta {
|
class SyncDelta {
|
||||||
SyncDelta({required this.hasChanges, required this.updates, required this.deletes, required this.assetAlbums});
|
SyncDelta({
|
||||||
|
required this.hasChanges,
|
||||||
|
required this.updates,
|
||||||
|
required this.deletes,
|
||||||
|
required this.assetAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
bool hasChanges;
|
bool hasChanges;
|
||||||
|
|
||||||
@@ -278,12 +288,16 @@ class SyncDelta {
|
|||||||
Map<String, List<String>> assetAlbums;
|
Map<String, List<String>> assetAlbums;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[hasChanges, updates, deletes, assetAlbums];
|
return <Object?>[
|
||||||
|
hasChanges,
|
||||||
|
updates,
|
||||||
|
deletes,
|
||||||
|
assetAlbums,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static SyncDelta decode(Object result) {
|
static SyncDelta decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
@@ -304,10 +318,7 @@ class SyncDelta {
|
|||||||
if (identical(this, other)) {
|
if (identical(this, other)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _deepEquals(hasChanges, other.hasChanges) &&
|
return _deepEquals(hasChanges, other.hasChanges) && _deepEquals(updates, other.updates) && _deepEquals(deletes, other.deletes) && _deepEquals(assetAlbums, other.assetAlbums);
|
||||||
_deepEquals(updates, other.updates) &&
|
|
||||||
_deepEquals(deletes, other.deletes) &&
|
|
||||||
_deepEquals(assetAlbums, other.assetAlbums);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -316,7 +327,11 @@ class SyncDelta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HashResult {
|
class HashResult {
|
||||||
HashResult({required this.assetId, this.error, this.hash});
|
HashResult({
|
||||||
|
required this.assetId,
|
||||||
|
this.error,
|
||||||
|
this.hash,
|
||||||
|
});
|
||||||
|
|
||||||
String assetId;
|
String assetId;
|
||||||
|
|
||||||
@@ -325,16 +340,23 @@ class HashResult {
|
|||||||
String? hash;
|
String? hash;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[assetId, error, hash];
|
return <Object?>[
|
||||||
|
assetId,
|
||||||
|
error,
|
||||||
|
hash,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static HashResult decode(Object result) {
|
static HashResult decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
return HashResult(assetId: result[0]! as String, error: result[1] as String?, hash: result[2] as String?);
|
return HashResult(
|
||||||
|
assetId: result[0]! as String,
|
||||||
|
error: result[1] as String?,
|
||||||
|
hash: result[2] as String?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -355,7 +377,11 @@ class HashResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CloudIdResult {
|
class CloudIdResult {
|
||||||
CloudIdResult({required this.assetId, this.error, this.cloudId});
|
CloudIdResult({
|
||||||
|
required this.assetId,
|
||||||
|
this.error,
|
||||||
|
this.cloudId,
|
||||||
|
});
|
||||||
|
|
||||||
String assetId;
|
String assetId;
|
||||||
|
|
||||||
@@ -364,16 +390,23 @@ class CloudIdResult {
|
|||||||
String? cloudId;
|
String? cloudId;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[assetId, error, cloudId];
|
return <Object?>[
|
||||||
|
assetId,
|
||||||
|
error,
|
||||||
|
cloudId,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static CloudIdResult decode(Object result) {
|
static CloudIdResult decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
return CloudIdResult(assetId: result[0]! as String, error: result[1] as String?, cloudId: result[2] as String?);
|
return CloudIdResult(
|
||||||
|
assetId: result[0]! as String,
|
||||||
|
error: result[1] as String?,
|
||||||
|
cloudId: result[2] as String?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -385,9 +418,7 @@ class CloudIdResult {
|
|||||||
if (identical(this, other)) {
|
if (identical(this, other)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _deepEquals(assetId, other.assetId) &&
|
return _deepEquals(assetId, other.assetId) && _deepEquals(error, other.error) && _deepEquals(cloudId, other.cloudId);
|
||||||
_deepEquals(error, other.error) &&
|
|
||||||
_deepEquals(cloudId, other.cloudId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -395,6 +426,7 @@ class CloudIdResult {
|
|||||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _PigeonCodec extends StandardMessageCodec {
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
const _PigeonCodec();
|
const _PigeonCodec();
|
||||||
@override
|
@override
|
||||||
@@ -402,22 +434,22 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
if (value is int) {
|
if (value is int) {
|
||||||
buffer.putUint8(4);
|
buffer.putUint8(4);
|
||||||
buffer.putInt64(value);
|
buffer.putInt64(value);
|
||||||
} else if (value is PlatformAssetPlaybackStyle) {
|
} else if (value is PlatformAssetPlaybackStyle) {
|
||||||
buffer.putUint8(129);
|
buffer.putUint8(129);
|
||||||
writeValue(buffer, value.index);
|
writeValue(buffer, value.index);
|
||||||
} else if (value is PlatformAsset) {
|
} else if (value is PlatformAsset) {
|
||||||
buffer.putUint8(130);
|
buffer.putUint8(130);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is PlatformAlbum) {
|
} else if (value is PlatformAlbum) {
|
||||||
buffer.putUint8(131);
|
buffer.putUint8(131);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is SyncDelta) {
|
} else if (value is SyncDelta) {
|
||||||
buffer.putUint8(132);
|
buffer.putUint8(132);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is HashResult) {
|
} else if (value is HashResult) {
|
||||||
buffer.putUint8(133);
|
buffer.putUint8(133);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is CloudIdResult) {
|
} else if (value is CloudIdResult) {
|
||||||
buffer.putUint8(134);
|
buffer.putUint8(134);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else {
|
} else {
|
||||||
@@ -452,8 +484,8 @@ class NativeSyncApi {
|
|||||||
/// available for dependency injection. If it is left null, the default
|
/// available for dependency injection. If it is left null, the default
|
||||||
/// BinaryMessenger will be used which routes to the host platform.
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
NativeSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
NativeSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
@@ -461,8 +493,7 @@ class NativeSyncApi {
|
|||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<bool> shouldFullSync() async {
|
Future<bool> shouldFullSync() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -472,16 +503,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return pigeonVar_replyValue! as bool;
|
return pigeonVar_replyValue! as bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SyncDelta> getMediaChanges() async {
|
Future<SyncDelta> getMediaChanges() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -491,16 +522,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return pigeonVar_replyValue! as SyncDelta;
|
return pigeonVar_replyValue! as SyncDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkpointSync() async {
|
Future<void> checkpointSync() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -509,12 +540,16 @@ class NativeSyncApi {
|
|||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
_extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: true,
|
||||||
|
)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearSyncCheckpoint() async {
|
Future<void> clearSyncCheckpoint() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -523,12 +558,16 @@ class NativeSyncApi {
|
|||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
_extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: true,
|
||||||
|
)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -538,16 +577,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
|
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PlatformAlbum>> getAlbums() async {
|
Future<List<PlatformAlbum>> getAlbums() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -557,16 +596,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAlbum>();
|
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAlbum>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
|
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -576,16 +615,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return pigeonVar_replyValue! as int;
|
return pigeonVar_replyValue! as int;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PlatformAsset>> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async {
|
Future<List<PlatformAsset>> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -595,16 +634,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAsset>();
|
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAsset>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<HashResult>> hashAssets(List<String> assetIds, {bool allowNetworkAccess = false}) async {
|
Future<List<HashResult>> hashAssets(List<String> assetIds, {bool allowNetworkAccess = false}) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -614,16 +653,16 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as List<Object?>).cast<HashResult>();
|
return (pigeonVar_replyValue! as List<Object?>).cast<HashResult>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancelHashing() async {
|
Future<void> cancelHashing() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -632,12 +671,16 @@ class NativeSyncApi {
|
|||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
_extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: true,
|
||||||
|
)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
|
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -647,16 +690,35 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
|
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 {
|
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -666,10 +728,11 @@ class NativeSyncApi {
|
|||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
pigeonVar_replyList,
|
pigeonVar_replyList,
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
isNullValid: false,
|
isNullValid: false,
|
||||||
);
|
)
|
||||||
|
;
|
||||||
return (pigeonVar_replyValue! as List<Object?>).cast<CloudIdResult>();
|
return (pigeonVar_replyValue! as List<Object?>).cast<CloudIdResult>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+19
@@ -309,4 +309,23 @@ class NetworkApi {
|
|||||||
|
|
||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getAppGroupId() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$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 String;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionApi {
|
||||||
|
/// Constructor for [PermissionApi]. 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.
|
||||||
|
PermissionApi({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<bool> hasManageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$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 bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestManageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$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 bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> manageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$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 bool;
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewIntentPayload {
|
||||||
|
ViewIntentPayload({this.path, required this.mimeType, this.localAssetId});
|
||||||
|
|
||||||
|
String? path;
|
||||||
|
|
||||||
|
String mimeType;
|
||||||
|
|
||||||
|
String? localAssetId;
|
||||||
|
|
||||||
|
List<Object?> _toList() {
|
||||||
|
return <Object?>[path, mimeType, localAssetId];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object encode() {
|
||||||
|
return _toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ViewIntentPayload decode(Object result) {
|
||||||
|
result as List<Object?>;
|
||||||
|
return ViewIntentPayload(
|
||||||
|
path: result[0] as String?,
|
||||||
|
mimeType: result[1]! as String,
|
||||||
|
localAssetId: result[2] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! ViewIntentPayload || other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _deepEquals(path, other.path) &&
|
||||||
|
_deepEquals(mimeType, other.mimeType) &&
|
||||||
|
_deepEquals(localAssetId, other.localAssetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 ViewIntentPayload) {
|
||||||
|
buffer.putUint8(129);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
case 129:
|
||||||
|
return ViewIntentPayload.decode(readValue(buffer)!);
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewIntentHostApi {
|
||||||
|
/// Constructor for [ViewIntentHostApi]. 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.
|
||||||
|
ViewIntentHostApi({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<ViewIntentPayload?> consumeViewIntent() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$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: true,
|
||||||
|
);
|
||||||
|
return pigeonVar_replyValue as ViewIntentPayload?;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/view_intent/view_intent_main_timeline_ready.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends ConsumerWidget {
|
class MainTimelinePage extends HookConsumerWidget {
|
||||||
const MainTimelinePage({super.key});
|
const MainTimelinePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
useEffect(() {
|
||||||
|
unawaited(Future<void>(() => ref.read(viewIntentMainTimelineReadyProvider.notifier).markMountedOnce()));
|
||||||
|
return null;
|
||||||
|
}, const []);
|
||||||
|
|
||||||
final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false));
|
final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false));
|
||||||
return Timeline(
|
return Timeline(
|
||||||
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
|
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
|
|
||||||
final scrollView = CustomScrollView(
|
final scrollView = CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
ImmichSliverAppBar(
|
ImmichSliverAppBar(
|
||||||
snap: false,
|
snap: false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
||||||
@@ -22,17 +21,13 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? [];
|
||||||
if (user == null) {
|
final timelineService = ref.watch(timelineFactoryProvider).main(timelineUsers);
|
||||||
throw Exception('User must be logged in to access asset selection timeline');
|
|
||||||
}
|
|
||||||
|
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: const Timeline(showStorageIndicator: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,17 +179,14 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final album = await ref
|
final album = await ref
|
||||||
.watch(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.createAlbum(
|
.createAlbumWithAssets(
|
||||||
title: title,
|
title: title,
|
||||||
description: albumDescriptionController.text.trim(),
|
description: albumDescriptionController.text.trim(),
|
||||||
assetIds: selectedAssets.map((asset) {
|
assets: selectedAssets,
|
||||||
final remoteAsset = asset as RemoteAsset;
|
|
||||||
return remoteAsset.id;
|
|
||||||
}).toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (album != null) {
|
if (album != null && context.mounted) {
|
||||||
unawaited(context.replaceRoute(RemoteAlbumRoute(album: album)));
|
unawaited(context.replaceRoute(RemoteAlbumRoute(album: album)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/pending_uploads_banner.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
@@ -39,7 +40,8 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAssets(BuildContext context) async {
|
Future<void> addAssets(BuildContext context) async {
|
||||||
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(_album.id);
|
final notifier = ref.read(remoteAlbumProvider.notifier);
|
||||||
|
final albumAssets = await notifier.getAssets(_album.id);
|
||||||
|
|
||||||
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
||||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
||||||
@@ -49,17 +51,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final added = await ref
|
final added = await notifier.addAssetsToAlbum(_album.id, newAssets);
|
||||||
.read(remoteAlbumProvider.notifier)
|
|
||||||
.addAssets(
|
|
||||||
_album.id,
|
|
||||||
newAssets.map((asset) {
|
|
||||||
final remoteAsset = asset as RemoteAsset;
|
|
||||||
return remoteAsset.id;
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (added > 0) {
|
if (added > 0 && context.mounted) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}),
|
msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}),
|
||||||
@@ -186,6 +180,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
currentRemoteAlbumScopedProvider.overrideWithValue(_album),
|
currentRemoteAlbumScopedProvider.overrideWithValue(_album),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
|
topSliverWidget: PendingUploadsBanner(albumId: _album.id),
|
||||||
appBar: RemoteAlbumSliverAppBar(
|
appBar: RemoteAlbumSliverAppBar(
|
||||||
icon: Icons.photo_album_outlined,
|
icon: Icons.photo_album_outlined,
|
||||||
kebabMenu: _AlbumKebabMenu(
|
kebabMenu: _AlbumKebabMenu(
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftSlideshowPage extends ConsumerStatefulWidget {
|
||||||
|
final TimelineService timeline;
|
||||||
|
|
||||||
|
const DriftSlideshowPage({super.key, required this.timeline});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftSlideshowPage> createState() => _DriftSlideshowPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> {
|
||||||
|
late SlideshowConfig _config;
|
||||||
|
late final PageController _pageController;
|
||||||
|
late final Stopwatch _stopwatch;
|
||||||
|
late Timer _timer;
|
||||||
|
late int _index;
|
||||||
|
late int _nextIndex;
|
||||||
|
bool _paused = false;
|
||||||
|
bool _showAppBar = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_config = ref.read(appConfigProvider.select((s) => s.slideshow));
|
||||||
|
final asset = ref.read(assetViewerProvider).currentAsset;
|
||||||
|
_index = asset == null ? 0 : widget.timeline.getIndex(asset.heroTag) ?? 0;
|
||||||
|
_pageController = PageController(initialPage: _index);
|
||||||
|
_stopwatch = Stopwatch();
|
||||||
|
_createTimer();
|
||||||
|
_updateNextIndex();
|
||||||
|
ref.listenManual(appConfigProvider.select((s) => s.slideshow), _onConfigChanged);
|
||||||
|
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
unawaited(WakelockPlus.enable());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_pageController.dispose();
|
||||||
|
unawaited(WakelockPlus.disable());
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _play() {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
} else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).play();
|
||||||
|
} else {
|
||||||
|
_nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onConfigChanged(SlideshowConfig? previous, SlideshowConfig next) {
|
||||||
|
if (_config == next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final durationChanged = _config.duration != next.duration;
|
||||||
|
_config = next;
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index);
|
||||||
|
if (durationChanged && !_paused && asset?.isImage == true) {
|
||||||
|
_timer.cancel();
|
||||||
|
_createTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateNextIndex() {
|
||||||
|
_nextIndex = switch (_config.direction) {
|
||||||
|
SlideshowDirection.forward => _index + 1,
|
||||||
|
SlideshowDirection.backward => _index - 1,
|
||||||
|
SlideshowDirection.shuffle => widget.timeline.getIndex(widget.timeline.getRandomAsset().heroTag)!,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextPage() async {
|
||||||
|
if (_nextIndex < 0 || _nextIndex >= widget.timeline.totalAssets) {
|
||||||
|
if (_config.repeat) {
|
||||||
|
final wrapped = _config.direction == SlideshowDirection.forward ? 0 : widget.timeline.totalAssets - 1;
|
||||||
|
await widget.timeline.preloadAssets(wrapped);
|
||||||
|
_pageController.jumpToPage(wrapped);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_paused = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
await widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.direction == SlideshowDirection.shuffle || !_config.transition) {
|
||||||
|
_pageController.jumpToPage(_nextIndex);
|
||||||
|
} else {
|
||||||
|
unawaited(_pageController.animateToPage(_nextIndex, duration: Durations.long2, curve: Curves.easeIn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createTimer() {
|
||||||
|
_timer = Timer(Duration(milliseconds: _config.duration * 1000 - _stopwatch.elapsedMilliseconds), () {
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
_nextPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
_stopwatch.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pageChanged(int page) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(page)!;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_index = page;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
_paused = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
|
||||||
|
if (!_paused && asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp() async {
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(_showAppBar ? SystemUiMode.immersive : SystemUiMode.edgeToEdge);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_showAppBar = !_showAppBar;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getProgressBar(BuildContext context) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
key: Key(_index.toString()),
|
||||||
|
tween: Tween<double>(begin: elapsed / duration.toDouble(), end: _paused ? elapsed / duration.toDouble() : 1.0),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value:
|
||||||
|
ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds /
|
||||||
|
asset.duration.inMilliseconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getBlur(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: getFullImageProvider(asset, size: Size(context.width, context.height)),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(color: Colors.black.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getPhotoView(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return const Center(child: ImmichLoadingIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final scale = _config.look == SlideshowLook.cover
|
||||||
|
? PhotoViewComputedScale.covered
|
||||||
|
: PhotoViewComputedScale.contained;
|
||||||
|
final isCurrent = _index == index;
|
||||||
|
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final zoomOut = index % 2 == 1;
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
final progress = zoomOut ? 1.0 - elapsed / duration.toDouble() : elapsed / duration.toDouble();
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: progress,
|
||||||
|
end: _paused
|
||||||
|
? progress
|
||||||
|
: zoomOut
|
||||||
|
? 0.0
|
||||||
|
: 1.0,
|
||||||
|
),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => PhotoView(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
index: index,
|
||||||
|
disableScaleGestures: true,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale * (1.0 + value / 10.0),
|
||||||
|
controller: PhotoViewController(),
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||||
|
final position = ref.read(videoPlayerProvider(asset.heroTag)).position;
|
||||||
|
|
||||||
|
if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) {
|
||||||
|
_nextPage();
|
||||||
|
} else if (status == VideoPlaybackStatus.playing) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhotoView.customChild(
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
disableScaleGestures: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale,
|
||||||
|
child: NativeVideoViewer(
|
||||||
|
asset: asset,
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: PreferredSize(
|
||||||
|
preferredSize: Size(AppBar().preferredSize.width, AppBar().preferredSize.height + 5),
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !_showAppBar,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _showAppBar ? 1.0 : 0.0,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
title: Text("slideshow".t(context: context)),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _paused ? _play : _pause,
|
||||||
|
icon: Icon(_paused ? Icons.play_arrow : Icons.pause),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_pause();
|
||||||
|
context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer));
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_getProgressBar(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
extendBody: true,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: PhotoViewGestureDetectorScope(
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const FastClampingScrollPhysics(),
|
||||||
|
itemCount: widget.timeline.totalAssets,
|
||||||
|
onPageChanged: _pageChanged,
|
||||||
|
itemBuilder: (context, index) => Stack(
|
||||||
|
children: [
|
||||||
|
if (_config.look == SlideshowLook.blurredBackground) _getBlur(context, index),
|
||||||
|
_getPhotoView(context, index),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -186,7 +186,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
onSearch: handleApply,
|
onSearch: handleApply,
|
||||||
onClear: handleClear,
|
onClear: handleClear,
|
||||||
child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
|
child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,10 +50,13 @@ class BaseActionButton extends ConsumerWidget {
|
|||||||
final iconColor = this.iconColor;
|
final iconColor = this.iconColor;
|
||||||
|
|
||||||
return MenuItemButton(
|
return MenuItemButton(
|
||||||
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
style: MenuItemButton.styleFrom(
|
||||||
leadingIcon: Icon(iconData, color: iconColor),
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
),
|
||||||
|
leadingIcon: Icon(iconData, color: iconColor, size: 20),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(label, style: TextStyle(fontSize: 16, color: iconColor)),
|
child: Text(label, style: TextStyle(fontSize: 15, color: iconColor)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class BulkTagAssetsActionButton extends ConsumerWidget {
|
||||||
|
final ActionSource source;
|
||||||
|
|
||||||
|
const BulkTagAssetsActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
final result = await ref.read(actionProvider.notifier).tagAssets(source, context);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? 'tagged_assets'.t(context: context, args: {'count': result.count.toString()})
|
||||||
|
: 'errors.failed_to_tag_assets'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.sell_outlined,
|
||||||
|
label: "control_bottom_app_bar_add_tags".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user