mirror of
https://github.com/immich-app/immich.git
synced 2026-01-11 20:55:25 -08:00
Compare commits
25 Commits
renovate/m
...
perf/optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee23794625 | ||
|
|
e8c80d88a5 | ||
|
|
76241a7b2b | ||
|
|
1e4af9731d | ||
|
|
88327fb872 | ||
|
|
702499b97d | ||
|
|
da248414af | ||
|
|
af2c232c87 | ||
|
|
cca037b03c | ||
|
|
1d71bb5a79 | ||
|
|
ee4f2c735d | ||
|
|
4d559a63ec | ||
|
|
573e9b0d52 | ||
|
|
a2502109ab | ||
|
|
3cdece4945 | ||
|
|
520b825511 | ||
|
|
191401f2f1 | ||
|
|
8136d7fd54 | ||
|
|
5d1e486478 | ||
|
|
85b0b97ef2 | ||
|
|
471fab0591 | ||
|
|
6997ed83c4 | ||
|
|
a2ba36c16d | ||
|
|
109c79125d | ||
|
|
fbd49e0b79 |
12
.github/workflows/build-mobile.yml
vendored
12
.github/workflows/build-mobile.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Restore Gradle Cache
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -153,14 +153,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Publish Android Artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: release-apk-signed
|
||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
security delete-keychain build.keychain || true
|
||||
|
||||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: ios-release-ipa
|
||||
path: mobile/ios/Runner.ipa
|
||||
|
||||
2
.github/workflows/cache-cleanup.yml
vendored
2
.github/workflows/cache-cleanup.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
4
.github/workflows/cli.yml
vendored
4
.github/workflows/cli.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
4
.github/workflows/docs-build.yml
vendored
4
.github/workflows/docs-build.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Upload build output
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: docs-build-output
|
||||
path: docs/build/
|
||||
|
||||
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
@@ -125,13 +125,13 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1
|
||||
|
||||
- name: Load parameters
|
||||
id: parameters
|
||||
|
||||
4
.github/workflows/docs-destroy.yml
vendored
4
.github/workflows/docs-destroy.yml
vendored
@@ -23,13 +23,13 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1
|
||||
|
||||
- name: Destroy Docs Subdomain
|
||||
env:
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
@@ -136,13 +136,13 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: release-apk-signed
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
4
.github/workflows/release-pr.yml
vendored
4
.github/workflows/release-pr.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: release-apk-signed
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
34
.github/workflows/test.yml
vendored
34
.github/workflows/test.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -285,7 +285,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -333,7 +333,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -379,7 +379,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -418,7 +418,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -473,7 +473,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
run: npx playwright test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-web-test-results-${{ matrix.runner }}
|
||||
@@ -534,7 +534,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -566,7 +566,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -607,7 +607,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -636,7 +636,7 @@ jobs:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -658,7 +658,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -33,7 +33,7 @@ You can create a public link to share a group of photos or videos, or an album,
|
||||
The public shared link is generated with a random URL, which acts as as a secret to avoid the link being guessed by unwanted parties, for instance.
|
||||
|
||||
```
|
||||
https://immich.yourdomain.com/share/JUckRMxlgpo7F9BpyqGk_cZEwDzaU_U5LU5_oNZp1ETIBa9dpQ0b5ghNm_22QVJfn3k
|
||||
https://my.immich.app/share/JUckRMxlgpo7F9BpyqGk_cZEwDzaU_U5LU5_oNZp1ETIBa9dpQ0b5ghNm_22QVJfn3k
|
||||
```
|
||||
|
||||
### Creating a public share link
|
||||
|
||||
6
e2e-auth-server/Dockerfile
Normal file
6
e2e-auth-server/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25
|
||||
RUN corepack enable
|
||||
ADD package.json *.ts ./
|
||||
RUN pnpm install
|
||||
EXPOSE 2286
|
||||
CMD ["pnpm", "run", "start"]
|
||||
@@ -125,7 +125,7 @@ const setup = async () => {
|
||||
],
|
||||
});
|
||||
|
||||
const onStart = () => console.log(`[auth-server] http://${host}:${port}/.well-known/openid-configuration`);
|
||||
const onStart = () => console.log(`[e2e-auth-server] http://${host}:${port}/.well-known/openid-configuration`);
|
||||
const app = oidc.listen(port, host, onStart);
|
||||
return () => app.close();
|
||||
};
|
||||
15
e2e-auth-server/package.json
Normal file
15
e2e-auth-server/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@immich/e2e-auth-server",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "auth-server.ts",
|
||||
"scripts": {
|
||||
"start": "tsx startup.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jose": "^5.6.3",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
}
|
||||
}
|
||||
8
e2e-auth-server/startup.ts
Normal file
8
e2e-auth-server/startup.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import setup from './auth-server'
|
||||
|
||||
const teardown = await setup()
|
||||
process.on('exit', () => {
|
||||
teardown()
|
||||
console.log('[e2e-auth-server] stopped')
|
||||
process.exit(0)
|
||||
})
|
||||
@@ -1,6 +1,12 @@
|
||||
name: immich-e2e
|
||||
|
||||
services:
|
||||
e2e-auth-server:
|
||||
build:
|
||||
context: ../e2e-auth-server
|
||||
ports:
|
||||
- 2286:2286
|
||||
|
||||
immich-server:
|
||||
container_name: immich-e2e-server
|
||||
image: immich-server:latest
|
||||
@@ -27,8 +33,6 @@ services:
|
||||
- IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
|
||||
volumes:
|
||||
- ./test-assets:/test-assets
|
||||
extra_hosts:
|
||||
- 'auth-server:host-gateway'
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/e2e-auth-server": "file:../e2e-auth-server",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@@ -38,9 +38,7 @@
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^34.3.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
"oidc-provider": "^9.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server';
|
||||
import {
|
||||
LoginResponseDto,
|
||||
SystemConfigOAuthDto,
|
||||
@@ -8,13 +9,12 @@ import {
|
||||
} from '@immich/sdk';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { OAuthClient, OAuthUser } from 'src/setup/auth-server';
|
||||
import { app, asBearerAuth, baseUrl, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const authServer = {
|
||||
internal: 'http://auth-server:2286',
|
||||
internal: 'http://e2e-auth-server:2286',
|
||||
external: 'http://127.0.0.1:2286',
|
||||
};
|
||||
|
||||
|
||||
@@ -346,6 +346,8 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
||||
duplicateId: null,
|
||||
resized: true,
|
||||
checksum: asset.checksum,
|
||||
width: exifInfo.exifImageWidth ?? 1,
|
||||
height: exifInfo.exifImageHeight ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AssetResponseDto } from '@immich/sdk';
|
||||
import { BrowserContext, Page, Request, Route } from '@playwright/test';
|
||||
import { basename } from 'node:path';
|
||||
import {
|
||||
@@ -63,15 +64,33 @@ export const setupTimelineMockApiRoutes = async (
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*', async (route, request) => {
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
const assetId = basename(pathname);
|
||||
const asset = getAsset(timelineRestData, assetId);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: asset,
|
||||
});
|
||||
if (request.method() === 'GET') {
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
const assetId = basename(pathname);
|
||||
let asset = getAsset(timelineRestData, assetId);
|
||||
if (changes.assetDeletions.includes(asset!.id)) {
|
||||
asset = {
|
||||
...asset,
|
||||
isTrashed: true,
|
||||
} as AssetResponseDto;
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: asset,
|
||||
});
|
||||
}
|
||||
await route.fallback();
|
||||
});
|
||||
|
||||
await context.route('**/api/assets', async (route, request) => {
|
||||
if (request.method() === 'DELETE') {
|
||||
return route.fulfill({
|
||||
status: 204,
|
||||
});
|
||||
}
|
||||
await route.fallback();
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*/ocr', async (route) => {
|
||||
@@ -117,17 +136,28 @@ export const setupTimelineMockApiRoutes = async (
|
||||
});
|
||||
|
||||
await context.route('**/api/albums/**', async (route, request) => {
|
||||
const pattern = /\/api\/albums\/(?<albumId>[^/?]+)/;
|
||||
const match = request.url().match(pattern);
|
||||
if (!match) {
|
||||
return route.continue();
|
||||
const albumsMatch = request.url().match(/\/api\/albums\/(?<albumId>[^/?]+)/);
|
||||
if (albumsMatch) {
|
||||
const album = getAlbum(timelineRestData, testContext.adminId, albumsMatch.groups?.albumId, changes);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: album,
|
||||
});
|
||||
}
|
||||
const album = getAlbum(timelineRestData, testContext.adminId, match.groups?.albumId, changes);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: album,
|
||||
});
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await context.route('**/api/albums**', async (route, request) => {
|
||||
const allAlbums = request.url().match(/\/api\/albums\?assetId=(?<assetId>[^&]+)/);
|
||||
if (allAlbums) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: [],
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
156
e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts
Normal file
156
e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { test } from '@playwright/test';
|
||||
import {
|
||||
Changes,
|
||||
createDefaultTimelineConfig,
|
||||
generateTimelineData,
|
||||
SeededRandom,
|
||||
selectRandom,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||
import { utils } from 'src/utils';
|
||||
import { assetViewerUtils, cancelAllPollers } from 'src/web/specs/timeline/utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.describe('asset-viewer', () => {
|
||||
const rng = new SeededRandom(529);
|
||||
let adminUserId: string;
|
||||
let timelineRestData: TimelineData;
|
||||
const assets: TimelineAssetConfig[] = [];
|
||||
const yearMonths: string[] = [];
|
||||
const testContext = new TimelineTestContext();
|
||||
const changes: Changes = {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
utils.initSdk();
|
||||
adminUserId = faker.string.uuid();
|
||||
testContext.adminId = adminUserId;
|
||||
timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId });
|
||||
for (const timeBucket of timelineRestData.buckets.values()) {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
for (const yearMonth of timelineRestData.buckets.keys()) {
|
||||
const [year, month] = yearMonth.split('-');
|
||||
yearMonths.push(`${year}-${Number(month)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
cancelAllPollers();
|
||||
testContext.slowBucket = false;
|
||||
changes.albumAdditions = [];
|
||||
changes.assetDeletions = [];
|
||||
changes.assetArchivals = [];
|
||||
changes.assetFavorites = [];
|
||||
});
|
||||
|
||||
test.describe('/photos/:id', () => {
|
||||
test('Delete photo advances to next', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
const index = assets.indexOf(asset);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
});
|
||||
test('Delete photo advances to next (2x)', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
const index = assets.indexOf(asset);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
await page.getByLabel('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 2]);
|
||||
});
|
||||
test('Delete last photo advances to prev', async ({ page }) => {
|
||||
const asset = assets.at(-1)!;
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
const index = assets.indexOf(asset);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||
});
|
||||
test('Delete last photo advances to prev (2x)', async ({ page }) => {
|
||||
const asset = assets.at(-1)!;
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
const index = assets.indexOf(asset);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||
await page.getByLabel('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - 2]);
|
||||
});
|
||||
});
|
||||
test.describe('/trash/photos/:id', () => {
|
||||
test('Delete trashed photo advances to next', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id);
|
||||
changes.assetDeletions.push(...deletedAssets);
|
||||
await page.goto(`/trash/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
});
|
||||
test('Delete trashed photo advances to next 2x', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id);
|
||||
changes.assetDeletions.push(...deletedAssets);
|
||||
await page.goto(`/trash/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 2]);
|
||||
});
|
||||
test('Delete trashed photo advances to prev', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id);
|
||||
changes.assetDeletions.push(...deletedAssets);
|
||||
await page.goto(`/trash/photos/${assets[index + 9].id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 9]);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 8]);
|
||||
});
|
||||
test('Delete trashed photo advances to prev 2x', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id);
|
||||
changes.assetDeletions.push(...deletedAssets);
|
||||
await page.goto(`/trash/photos/${assets[index + 9].id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 9]);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 8]);
|
||||
await page.getByLabel('Delete').click();
|
||||
// confirm dialog
|
||||
await page.getByRole('button').getByText('Delete').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 7]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -463,7 +463,7 @@ test.describe('Timeline', () => {
|
||||
});
|
||||
changes.albumAdditions.push(...requestJson.ids);
|
||||
});
|
||||
await page.getByText('Done').click();
|
||||
await page.getByText('Add assets').click();
|
||||
await expect(put).resolves.toEqual({
|
||||
ids: [
|
||||
'c077ea7b-cfa1-45e4-8554-f86c00ee5658',
|
||||
|
||||
@@ -181,8 +181,12 @@ export const assetViewerUtils = {
|
||||
},
|
||||
async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) {
|
||||
await page
|
||||
.locator(`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}"]`)
|
||||
.or(page.locator(`video[poster="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}"]`))
|
||||
.locator(
|
||||
`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`,
|
||||
)
|
||||
.or(
|
||||
page.locator(`video[poster="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`),
|
||||
)
|
||||
.waitFor();
|
||||
},
|
||||
async expectActiveAssetToBe(page: Page, assetId: string) {
|
||||
|
||||
@@ -56,7 +56,7 @@ test.describe('User Administration', () => {
|
||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||
await page.getByLabel('Admin User').click();
|
||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -85,7 +85,7 @@ test.describe('User Administration', () => {
|
||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||
await page.getByLabel('Admin User').click();
|
||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// skip `docker compose up` if `make e2e` was already run
|
||||
const globalSetup: string[] = ['src/setup/auth-server.ts'];
|
||||
const globalSetup: string[] = [];
|
||||
try {
|
||||
await fetch('http://127.0.0.1:2285/api/server-info/ping');
|
||||
} catch {
|
||||
|
||||
17
i18n/en.json
17
i18n/en.json
@@ -18,6 +18,7 @@
|
||||
"add_a_title": "Add a title",
|
||||
"add_action": "Add action",
|
||||
"add_action_description": "Click to add an action to perform",
|
||||
"add_assets": "Add assets",
|
||||
"add_birthday": "Add a birthday",
|
||||
"add_endpoint": "Add endpoint",
|
||||
"add_exclusion_pattern": "Add exclusion pattern",
|
||||
@@ -478,6 +479,7 @@
|
||||
"album_summary": "Album summary",
|
||||
"album_updated": "Album updated",
|
||||
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
|
||||
"album_upload_assets": "Upload assets from your computer and add to album",
|
||||
"album_user_left": "Left {album}",
|
||||
"album_user_removed": "Removed {user}",
|
||||
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?",
|
||||
@@ -831,6 +833,9 @@
|
||||
"created_at": "Created",
|
||||
"creating_linked_albums": "Creating linked albums...",
|
||||
"crop": "Crop",
|
||||
"crop_aspect_ratio_fixed": "Fixed",
|
||||
"crop_aspect_ratio_free": "Free",
|
||||
"crop_aspect_ratio_original": "Original",
|
||||
"curated_object_page_title": "Things",
|
||||
"current_device": "Current device",
|
||||
"current_pin_code": "Current PIN code",
|
||||
@@ -964,9 +969,13 @@
|
||||
"editor": "Editor",
|
||||
"editor_close_without_save_prompt": "The changes will not be saved",
|
||||
"editor_close_without_save_title": "Close editor?",
|
||||
"editor_crop_tool_h2_aspect_ratios": "Aspect ratios",
|
||||
"editor_crop_tool_h2_rotation": "Rotation",
|
||||
"editor_mode": "Editor mode",
|
||||
"editor_confirm_reset_all_changes": "Are you sure you want to reset all changes?",
|
||||
"editor_flip_horizontal": "Flip horizontal",
|
||||
"editor_flip_vertical": "Flip vertical",
|
||||
"editor_orientation": "Orientation",
|
||||
"editor_reset_all_changes": "Reset changes",
|
||||
"editor_rotate_left": "Rotate 90° counterclockwise",
|
||||
"editor_rotate_right": "Rotate 90° clockwise",
|
||||
"email": "Email",
|
||||
"email_notifications": "Email notifications",
|
||||
"empty_folder": "This folder is empty",
|
||||
@@ -1457,6 +1466,8 @@
|
||||
"minimize": "Minimize",
|
||||
"minute": "Minute",
|
||||
"minutes": "Minutes",
|
||||
"mirror_horizontal": "Horizontal",
|
||||
"mirror_vertical": "Vertical",
|
||||
"missing": "Missing",
|
||||
"mobile_app": "Mobile App",
|
||||
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.38.5"
|
||||
"flutter": "3.35.7"
|
||||
}
|
||||
@@ -7,7 +7,7 @@ environment:
|
||||
dependencies:
|
||||
analyzer: ^7.0.0
|
||||
analyzer_plugin: ^0.13.0
|
||||
custom_lint_builder: 0.8.1
|
||||
custom_lint_builder: ^0.7.5
|
||||
glob: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:immich_mobile/domain/models/exif.model.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/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
|
||||
typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped});
|
||||
|
||||
@@ -99,9 +98,7 @@ class AssetService {
|
||||
height = fetched?.height?.toDouble();
|
||||
}
|
||||
|
||||
final exif = await getExif(asset);
|
||||
final isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
||||
return (width: width, height: height, isFlipped: isFlipped);
|
||||
return (width: width, height: height, isFlipped: false);
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces(String userId) {
|
||||
|
||||
@@ -171,16 +171,8 @@ class RemoteAlbumService {
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their newest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
||||
for (final album in albums) {
|
||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
||||
}
|
||||
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
final albumIds = albums.map((e) => e.id).toList();
|
||||
final assetTimestamps = await _repository.getNewestAssetTimestampForAlbums(albumIds);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
@@ -193,15 +185,8 @@ class RemoteAlbumService {
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their oldest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {
|
||||
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
|
||||
};
|
||||
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
final albumIds = albums.map((e) => e.id).toList();
|
||||
final assetTimestamps = await _repository.getOldestAssetTimestampForAlbums(albumIds);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@@ -321,26 +321,64 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
Future<Map<String, DateTime?>> getNewestAssetTimestampForAlbums(List<String> albumIds) async {
|
||||
if (albumIds.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
||||
final results = <String, DateTime?>{};
|
||||
|
||||
// Chunk calls to avoid SQLite limit (default 999 variables)
|
||||
const chunkSize = 900;
|
||||
for (var i = 0; i < albumIds.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < albumIds.length) ? i + chunkSize : albumIds.length;
|
||||
final subList = albumIds.sublist(i, end);
|
||||
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.isIn(subList))
|
||||
..addColumns([_db.remoteAlbumAssetEntity.albumId, _db.remoteAssetEntity.localDateTime.max()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
])
|
||||
..groupBy([_db.remoteAlbumAssetEntity.albumId]);
|
||||
|
||||
final rows = await query.get();
|
||||
for (final row in rows) {
|
||||
results[row.read(_db.remoteAlbumAssetEntity.albumId)!] = row.read(_db.remoteAssetEntity.localDateTime.max());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
Future<Map<String, DateTime?>> getOldestAssetTimestampForAlbums(List<String> albumIds) async {
|
||||
if (albumIds.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
||||
final results = <String, DateTime?>{};
|
||||
|
||||
// Chunk calls to avoid SQLite limit (default 999 variables)
|
||||
const chunkSize = 900;
|
||||
for (var i = 0; i < albumIds.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < albumIds.length) ? i + chunkSize : albumIds.length;
|
||||
final subList = albumIds.sublist(i, end);
|
||||
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.isIn(subList))
|
||||
..addColumns([_db.remoteAlbumAssetEntity.albumId, _db.remoteAssetEntity.localDateTime.min()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
])
|
||||
..groupBy([_db.remoteAlbumAssetEntity.albumId]);
|
||||
|
||||
final rows = await query.get();
|
||||
for (final row in rows) {
|
||||
results[row.read(_db.remoteAlbumAssetEntity.albumId)!] = row.read(_db.remoteAssetEntity.localDateTime.min());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<int> getCount() {
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||
@@ -194,6 +195,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||
stackId: Value(asset.stackId),
|
||||
libraryId: Value(asset.libraryId),
|
||||
width: Value(asset.width),
|
||||
height: Value(asset.height),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
@@ -245,10 +248,21 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
|
||||
await _db.batch((batch) {
|
||||
for (final exif in data) {
|
||||
int? width;
|
||||
int? height;
|
||||
|
||||
if (ExifDtoConverter.isOrientationFlipped(exif.orientation)) {
|
||||
width = exif.exifImageHeight;
|
||||
height = exif.exifImageWidth;
|
||||
} else {
|
||||
width = exif.exifImageWidth;
|
||||
height = exif.exifImageHeight;
|
||||
}
|
||||
|
||||
batch.update(
|
||||
_db.remoteAssetEntity,
|
||||
RemoteAssetEntityCompanion(width: Value(exif.exifImageWidth), height: Value(exif.exifImageHeight)),
|
||||
where: (row) => row.id.equals(exif.assetId),
|
||||
RemoteAssetEntityCompanion(width: Value(width), height: Value(height)),
|
||||
where: (row) => row.id.equals(exif.assetId) & row.width.isNull() & row.height.isNull(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/utils/licenses.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
@@ -252,6 +253,13 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
themeMode: ref.watch(immichThemeModeProvider),
|
||||
darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale),
|
||||
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
|
||||
builder: (context, child) => ImmichTranslationProvider(
|
||||
translations: ImmichTranslations(
|
||||
submit: "submit".t(context: context),
|
||||
password: "password".t(context: context),
|
||||
),
|
||||
child: ImmichThemeProvider(colorScheme: context.colorScheme, child: child!),
|
||||
),
|
||||
routerConfig: router.config(
|
||||
deepLinkBuilder: _deepLinkBuilder,
|
||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
||||
|
||||
@@ -370,6 +370,7 @@ class _MapWithMarker extends StatelessWidget {
|
||||
? PositionedAssetMarkerIcon(
|
||||
point: value.point,
|
||||
assetRemoteId: value.marker.assetRemoteId,
|
||||
assetThumbhash: '',
|
||||
durationInMilliseconds: value.shouldAnimate ? 100 : 0,
|
||||
onTap: onMarkerTapped,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,17 @@ List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color)
|
||||
return children;
|
||||
}
|
||||
|
||||
class _ComponentTitle extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _ComponentTitle(this.title);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(title, style: context.textTheme.titleLarge);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class ImmichUIShowcasePage extends StatelessWidget {
|
||||
const ImmichUIShowcasePage({super.key});
|
||||
@@ -35,13 +46,51 @@ class ImmichUIShowcasePage extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("IconButton", style: context.textTheme.titleLarge),
|
||||
const _ComponentTitle("IconButton"),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) =>
|
||||
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onTap: () {}),
|
||||
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onPressed: () {}),
|
||||
),
|
||||
const _ComponentTitle("CloseButton"),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) => ImmichCloseButton(color: color, variant: variant, onPressed: () {}),
|
||||
),
|
||||
const _ComponentTitle("TextButton"),
|
||||
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.filled,
|
||||
color: ImmichColor.primary,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.filled,
|
||||
color: ImmichColor.primary,
|
||||
loading: true,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.primary,
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: "Text Button",
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.primary,
|
||||
loading: true,
|
||||
),
|
||||
const _ComponentTitle("Form"),
|
||||
ImmichForm(
|
||||
onSubmit: () {},
|
||||
child: const Column(
|
||||
spacing: 10,
|
||||
children: [ImmichTextInput(label: "Title", hintText: "Enter a title")],
|
||||
),
|
||||
),
|
||||
Text("CloseButton", style: context.textTheme.titleLarge),
|
||||
..._showcaseBuilder((variant, color) => ImmichCloseButton(color: color, variant: variant, onTap: () {})),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ class DriftCropImagePage extends HookWidget {
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onTap: () async {
|
||||
onPressed: () async {
|
||||
final croppedImage = await cropController.croppedImage();
|
||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
||||
},
|
||||
@@ -79,13 +79,13 @@ class DriftCropImagePage extends HookWidget {
|
||||
icon: Icons.rotate_left,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateLeft(),
|
||||
onPressed: () => cropController.rotateLeft(),
|
||||
),
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_right,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateRight(),
|
||||
onPressed: () => cropController.rotateRight(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -611,6 +611,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
filterQuality: FilterQuality.high,
|
||||
maxScale: 1.0,
|
||||
basePosition: Alignment.center,
|
||||
disableScaleGestures: true,
|
||||
child: SizedBox(
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
|
||||
@@ -68,7 +68,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final remoteId = asset is LocalAsset ? asset.remoteId : (asset as RemoteAsset).id;
|
||||
final remoteAsset = asset as RemoteAsset;
|
||||
final locationName = _getLocationName(exifInfo);
|
||||
final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}";
|
||||
|
||||
@@ -92,7 +92,12 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated),
|
||||
ExifMap(
|
||||
exifInfo: exifInfo!,
|
||||
markerId: remoteAsset.id,
|
||||
markerAssetThumbhash: remoteAsset.thumbHash,
|
||||
onMapCreated: _onMapCreated,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (locationName != null)
|
||||
Padding(
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_
|
||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
||||
@@ -93,7 +94,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final request = this.request = RemoteImageRequest(
|
||||
uri: getPreviewUrlForRemoteId(key.assetId),
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview),
|
||||
headers: headers,
|
||||
cacheManager: cacheManager,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
@@ -10,14 +9,18 @@ String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.
|
||||
}
|
||||
|
||||
String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type);
|
||||
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type);
|
||||
}
|
||||
|
||||
String getThumbnailCacheKeyForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
String getThumbnailCacheKeyForRemoteId(
|
||||
final String id,
|
||||
final String thumbhash, {
|
||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||
}) {
|
||||
if (type == AssetMediaSize.thumbnail) {
|
||||
return 'thumbnail-image-$id';
|
||||
return 'thumbnail-image-$id-$thumbhash';
|
||||
} else {
|
||||
return '${id}_previewStage';
|
||||
return '${id}_${thumbhash}_previewStage';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,26 +35,25 @@ String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = Asset
|
||||
if (album.thumbnail.value?.remoteId == null) {
|
||||
return '';
|
||||
}
|
||||
return getThumbnailCacheKeyForRemoteId(album.thumbnail.value!.remoteId!, type: type);
|
||||
return getThumbnailCacheKeyForRemoteId(
|
||||
album.thumbnail.value!.remoteId!,
|
||||
album.thumbnail.value!.thumbhash!,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
String getOriginalUrlForRemoteId(final String id) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original';
|
||||
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited';
|
||||
}
|
||||
|
||||
String getImageCacheKey(final Asset asset) {
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromDto = asset.id == noDbId;
|
||||
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
|
||||
String getThumbnailUrlForRemoteId(
|
||||
final String id, {
|
||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||
bool edited = true,
|
||||
}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
|
||||
}
|
||||
|
||||
String getThumbnailUrlForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
|
||||
}
|
||||
|
||||
String getPreviewUrlForRemoteId(final String id) =>
|
||||
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}';
|
||||
|
||||
String getPlaybackUrlForRemoteId(final String id) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class AssetLocation extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16),
|
||||
ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId),
|
||||
ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash),
|
||||
const SizedBox(height: 16),
|
||||
getLocationName(),
|
||||
Text(
|
||||
|
||||
@@ -10,10 +10,20 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ExifMap extends StatelessWidget {
|
||||
final ExifInfo exifInfo;
|
||||
// TODO: Pass in a BaseAsset instead of the ID and thumbhash when removing old timeline
|
||||
// This is currently structured this way because of the old timeline implementation
|
||||
// reusing this component
|
||||
final String? markerId;
|
||||
final String? markerAssetThumbhash;
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
|
||||
const ExifMap({super.key, required this.exifInfo, this.markerId = 'marker', this.onMapCreated});
|
||||
const ExifMap({
|
||||
super.key,
|
||||
required this.exifInfo,
|
||||
this.markerAssetThumbhash,
|
||||
this.markerId = 'marker',
|
||||
this.onMapCreated,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -61,6 +71,7 @@ class ExifMap extends StatelessWidget {
|
||||
width: constraints.maxWidth,
|
||||
zoom: 12.0,
|
||||
assetMarkerRemoteId: markerId,
|
||||
assetThumbhash: markerAssetThumbhash,
|
||||
onTap: (tapPosition, latLong) async {
|
||||
Uri? uri = await createCoordinatesUri();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
@@ -29,12 +30,7 @@ import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -45,16 +41,33 @@ class LoginForm extends HookConsumerWidget {
|
||||
|
||||
final log = Logger('LoginForm');
|
||||
|
||||
String? _validateUrl(String? url) {
|
||||
if (url == null || url.isEmpty) return null;
|
||||
|
||||
final parsedUrl = Uri.tryParse(url);
|
||||
if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) {
|
||||
return 'login_form_err_invalid_url'.tr();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateEmail(String? email) {
|
||||
if (email == null || email == '') return null;
|
||||
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
|
||||
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
|
||||
if (email.contains(' ') || !email.contains('@')) {
|
||||
return 'login_form_err_invalid_email'.tr();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final emailController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||
final serverEndpointController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||
final emailFocusNode = useFocusNode();
|
||||
final passwordFocusNode = useFocusNode();
|
||||
final serverEndpointFocusNode = useFocusNode();
|
||||
final isLoading = useState<bool>(false);
|
||||
final isLoadingServer = useState<bool>(false);
|
||||
final isOauthEnable = useState<bool>(false);
|
||||
final isPasswordLoginEnable = useState<bool>(false);
|
||||
final oAuthButtonLabel = useState<String>('OAuth');
|
||||
@@ -96,7 +109,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
try {
|
||||
isLoadingServer.value = true;
|
||||
final endpoint = await ref.read(authProvider.notifier).validateServerUrl(serverUrl);
|
||||
|
||||
// Fetch and load server config and features
|
||||
@@ -120,7 +132,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
);
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
isLoadingServer.value = false;
|
||||
} on HandshakeException {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -130,7 +141,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
);
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
isLoadingServer.value = false;
|
||||
} catch (e) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -140,10 +150,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
);
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
isLoadingServer.value = false;
|
||||
}
|
||||
|
||||
isLoadingServer.value = false;
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
@@ -230,8 +237,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
login() async {
|
||||
TextInput.finishAutofillContext();
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// Invalidate all api repository provider instance to take into account new access token
|
||||
invalidateAllApiRepositoryProviders(ref);
|
||||
|
||||
@@ -261,8 +266,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +309,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
codeChallenge,
|
||||
);
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// Invalidate all api repository provider instance to take into account new access token
|
||||
invalidateAllApiRepositoryProviders(ref);
|
||||
} catch (error, stack) {
|
||||
@@ -319,7 +320,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.TOP,
|
||||
);
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,7 +338,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
.saveAuthInfo(accessToken: loginResponseDto.accessToken);
|
||||
|
||||
if (isSuccess) {
|
||||
isLoading.value = false;
|
||||
final permission = ref.watch(galleryPermissionNotifier);
|
||||
final isBeta = Store.isBetaTimelineEnabled;
|
||||
if (!isBeta && (permission.isGranted || permission.isLimited)) {
|
||||
@@ -364,9 +363,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.TOP,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
} finally {}
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -374,66 +371,10 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.info,
|
||||
gravity: ToastGravity.TOP,
|
||||
);
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
buildSelectServer() {
|
||||
const buttonRadius = 25.0;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ServerEndpointInput(
|
||||
controller: serverEndpointController,
|
||||
focusNode: serverEndpointFocusNode,
|
||||
onSubmit: getServerAuthSettings,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(buttonRadius),
|
||||
bottomLeft: Radius.circular(buttonRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () => context.pushRoute(const SettingsRoute()),
|
||||
icon: const Icon(Icons.settings_rounded),
|
||||
label: const Text(""),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(buttonRadius),
|
||||
bottomRight: Radius.circular(buttonRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: isLoadingServer.value ? null : getServerAuthSettings,
|
||||
icon: const Icon(Icons.arrow_forward_rounded),
|
||||
label: const Text('next', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
if (isLoadingServer.value) const LoadingIcon(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
buildVersionCompatWarning() {
|
||||
checkVersionMismatch();
|
||||
|
||||
@@ -455,66 +396,102 @@ class LoginForm extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
buildLogin() {
|
||||
return AutofillGroup(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildVersionCompatWarning(),
|
||||
Text(
|
||||
sanitizeUrl(serverEndpointController.text),
|
||||
style: context.textTheme.displaySmall,
|
||||
textAlign: TextAlign.center,
|
||||
final serverSelectionOrLogin = serverEndpoint.value == null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: ImmichSpacing.md),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ImmichForm(
|
||||
submitText: 'next'.t(context: context),
|
||||
submitIcon: Icons.arrow_forward_rounded,
|
||||
onSubmit: getServerAuthSettings,
|
||||
child: ImmichTextInput(
|
||||
controller: serverEndpointController,
|
||||
label: 'login_form_endpoint_url'.t(context: context),
|
||||
hintText: 'login_form_endpoint_hint'.t(context: context),
|
||||
validator: _validateUrl,
|
||||
keyboardAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
|
||||
),
|
||||
),
|
||||
ImmichTextButton(
|
||||
labelText: 'settings'.t(context: context),
|
||||
icon: Icons.settings,
|
||||
variant: ImmichVariant.ghost,
|
||||
onPressed: () => context.pushRoute(const SettingsRoute()),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isPasswordLoginEnable.value) ...[
|
||||
const SizedBox(height: 18),
|
||||
EmailInput(
|
||||
controller: emailController,
|
||||
focusNode: emailFocusNode,
|
||||
onSubmit: passwordFocusNode.requestFocus,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PasswordInput(controller: passwordController, focusNode: passwordFocusNode, onSubmit: login),
|
||||
],
|
||||
|
||||
// Note: This used to have an AnimatedSwitcher, but was removed
|
||||
// because of https://github.com/flutter/flutter/issues/120874
|
||||
isLoading.value
|
||||
? const LoadingIcon()
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 18),
|
||||
if (isPasswordLoginEnable.value) LoginButton(onPressed: login),
|
||||
if (isOauthEnable.value) ...[
|
||||
if (isPasswordLoginEnable.value)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black),
|
||||
),
|
||||
OAuthLoginButton(
|
||||
serverEndpointController: serverEndpointController,
|
||||
buttonLabel: oAuthButtonLabel.value,
|
||||
isLoading: isLoading,
|
||||
onPressed: oAuthLogin,
|
||||
)
|
||||
: AutofillGroup(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
buildVersionCompatWarning(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: ImmichSpacing.md),
|
||||
child: Text(
|
||||
sanitizeUrl(serverEndpointController.text),
|
||||
style: context.textTheme.displaySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (isPasswordLoginEnable.value)
|
||||
ImmichForm(
|
||||
submitText: 'login'.t(context: context),
|
||||
submitIcon: Icons.login_rounded,
|
||||
onSubmit: login,
|
||||
child: Column(
|
||||
spacing: ImmichSpacing.md,
|
||||
children: [
|
||||
ImmichTextInput(
|
||||
controller: emailController,
|
||||
label: 'email'.t(context: context),
|
||||
hintText: 'login_form_email_hint'.t(context: context),
|
||||
validator: _validateEmail,
|
||||
keyboardAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
onSubmit: (_, _) => passwordFocusNode.requestFocus(),
|
||||
),
|
||||
ImmichPasswordInput(
|
||||
controller: passwordController,
|
||||
focusNode: passwordFocusNode,
|
||||
label: 'password'.t(context: context),
|
||||
hintText: 'login_form_password_hint'.t(context: context),
|
||||
keyboardAction: TextInputAction.go,
|
||||
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isOauthEnable.value && !isPasswordLoginEnable.value) Center(child: const Text('login_disabled').tr()),
|
||||
const SizedBox(height: 12),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => serverEndpoint.value = null,
|
||||
label: const Text('back').tr(),
|
||||
if (isOauthEnable.value)
|
||||
ImmichForm(
|
||||
submitText: oAuthButtonLabel.value,
|
||||
submitIcon: Icons.pin_outlined,
|
||||
onSubmit: oAuthLogin,
|
||||
child: isPasswordLoginEnable.value
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0),
|
||||
child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
if (!isOauthEnable.value && !isPasswordLoginEnable.value)
|
||||
Center(child: const Text('login_disabled').tr()),
|
||||
ImmichTextButton(
|
||||
labelText: 'back'.t(context: context),
|
||||
icon: Icons.arrow_back,
|
||||
variant: ImmichVariant.ghost,
|
||||
onPressed: () => serverEndpoint.value = null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final serverSelectionOrLogin = serverEndpoint.value == null ? buildSelectServer() : buildLogin();
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class OAuthLoginButton extends ConsumerWidget {
|
||||
final TextEditingController serverEndpointController;
|
||||
final ValueNotifier<bool> isLoading;
|
||||
final String buttonLabel;
|
||||
final Function() onPressed;
|
||||
|
||||
const OAuthLoginButton({
|
||||
super.key,
|
||||
required this.serverEndpointController,
|
||||
required this.isLoading,
|
||||
required this.buttonLabel,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.primaryColor.withAlpha(230),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.pin_rounded),
|
||||
label: Text(buttonLabel, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class PasswordInput extends HookConsumerWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
final Function()? onSubmit;
|
||||
|
||||
const PasswordInput({super.key, required this.controller, this.focusNode, this.onSubmit});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isPasswordVisible = useState<bool>(false);
|
||||
|
||||
return TextFormField(
|
||||
obscureText: !isPasswordVisible.value,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'password'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'login_form_password_hint'.tr(),
|
||||
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
|
||||
icon: Icon(isPasswordVisible.value ? Icons.visibility_off_sharp : Icons.visibility_sharp),
|
||||
),
|
||||
),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
keyboardType: TextInputType.text,
|
||||
onFieldSubmitted: (_) => onSubmit?.call(),
|
||||
focusNode: focusNode,
|
||||
textInputAction: TextInputAction.go,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
class ServerEndpointInput extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
final Function()? onSubmit;
|
||||
|
||||
const ServerEndpointInput({super.key, required this.controller, required this.focusNode, this.onSubmit});
|
||||
|
||||
String? _validateInput(String? url) {
|
||||
if (url == null || url.isEmpty) return null;
|
||||
|
||||
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
|
||||
if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) {
|
||||
return 'login_form_err_invalid_url'.tr();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'login_form_endpoint_url'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'login_form_endpoint_hint'.tr(),
|
||||
errorMaxLines: 4,
|
||||
),
|
||||
validator: _validateInput,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
focusNode: focusNode,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
onFieldSubmitted: (_) => onSubmit?.call(),
|
||||
textInputAction: TextInputAction.go,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
final Function(Point<double>, LatLng)? onTap;
|
||||
final LatLng centre;
|
||||
final String? assetMarkerRemoteId;
|
||||
final String? assetThumbhash;
|
||||
final bool showMarkerPin;
|
||||
final double zoom;
|
||||
final double height;
|
||||
@@ -35,6 +36,7 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
this.onTap,
|
||||
this.zoom = 8,
|
||||
this.assetMarkerRemoteId,
|
||||
this.assetThumbhash,
|
||||
this.showMarkerPin = false,
|
||||
this.themeMode,
|
||||
this.showAttribution = true,
|
||||
@@ -109,8 +111,13 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: position,
|
||||
builder: (_, value, __) => value != null && assetMarkerRemoteId != null
|
||||
? PositionedAssetMarkerIcon(size: height / 2, point: value, assetRemoteId: assetMarkerRemoteId!)
|
||||
builder: (_, value, __) => value != null && assetMarkerRemoteId != null && assetThumbhash != null
|
||||
? PositionedAssetMarkerIcon(
|
||||
size: height / 2,
|
||||
point: value,
|
||||
assetRemoteId: assetMarkerRemoteId!,
|
||||
assetThumbhash: assetThumbhash!,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||
final Point<num> point;
|
||||
final String assetRemoteId;
|
||||
final String assetThumbhash;
|
||||
final double size;
|
||||
final int durationInMilliseconds;
|
||||
|
||||
@@ -18,6 +19,7 @@ class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||
const PositionedAssetMarkerIcon({
|
||||
required this.point,
|
||||
required this.assetRemoteId,
|
||||
required this.assetThumbhash,
|
||||
this.size = 100,
|
||||
this.durationInMilliseconds = 100,
|
||||
this.onTap,
|
||||
@@ -35,7 +37,7 @@ class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||
onTap: () => onTap?.call(),
|
||||
child: SizedBox.square(
|
||||
dimension: size,
|
||||
child: _AssetMarkerIcon(id: assetRemoteId, key: Key(assetRemoteId)),
|
||||
child: _AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -43,14 +45,15 @@ class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AssetMarkerIcon extends StatelessWidget {
|
||||
const _AssetMarkerIcon({required this.id, super.key});
|
||||
const _AssetMarkerIcon({required this.id, required this.thumbhash, super.key});
|
||||
|
||||
final String id;
|
||||
final String thumbhash;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageUrl = getThumbnailUrlForRemoteId(id);
|
||||
final cacheKey = getThumbnailCacheKeyForRemoteId(id);
|
||||
final cacheKey = getThumbnailCacheKeyForRemoteId(id, thumbhash);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Stack(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tools]
|
||||
flutter = "3.38.5"
|
||||
flutter = "3.35.7"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
22
mobile/openapi/README.md
generated
22
mobile/openapi/README.md
generated
@@ -100,8 +100,11 @@ Class | Method | HTTP request | Description
|
||||
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset
|
||||
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key
|
||||
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets
|
||||
*AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata
|
||||
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset
|
||||
*AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset
|
||||
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
|
||||
*AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset
|
||||
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset
|
||||
*AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata
|
||||
*AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key
|
||||
@@ -109,11 +112,13 @@ Class | Method | HTTP request | Description
|
||||
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics
|
||||
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets
|
||||
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video
|
||||
*AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset
|
||||
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset
|
||||
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job
|
||||
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset
|
||||
*AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata
|
||||
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | Update assets
|
||||
*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata
|
||||
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset
|
||||
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password
|
||||
@@ -344,6 +349,13 @@ Class | Method | HTTP request | Description
|
||||
- [AssetCopyDto](doc//AssetCopyDto.md)
|
||||
- [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md)
|
||||
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
|
||||
- [AssetEditAction](doc//AssetEditAction.md)
|
||||
- [AssetEditActionCrop](doc//AssetEditActionCrop.md)
|
||||
- [AssetEditActionListDto](doc//AssetEditActionListDto.md)
|
||||
- [AssetEditActionListDtoEditsInner](doc//AssetEditActionListDtoEditsInner.md)
|
||||
- [AssetEditActionMirror](doc//AssetEditActionMirror.md)
|
||||
- [AssetEditActionRotate](doc//AssetEditActionRotate.md)
|
||||
- [AssetEditsDto](doc//AssetEditsDto.md)
|
||||
- [AssetFaceCreateDto](doc//AssetFaceCreateDto.md)
|
||||
- [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md)
|
||||
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
|
||||
@@ -358,7 +370,11 @@ Class | Method | HTTP request | Description
|
||||
- [AssetMediaResponseDto](doc//AssetMediaResponseDto.md)
|
||||
- [AssetMediaSize](doc//AssetMediaSize.md)
|
||||
- [AssetMediaStatus](doc//AssetMediaStatus.md)
|
||||
- [AssetMetadataKey](doc//AssetMetadataKey.md)
|
||||
- [AssetMetadataBulkDeleteDto](doc//AssetMetadataBulkDeleteDto.md)
|
||||
- [AssetMetadataBulkDeleteItemDto](doc//AssetMetadataBulkDeleteItemDto.md)
|
||||
- [AssetMetadataBulkResponseDto](doc//AssetMetadataBulkResponseDto.md)
|
||||
- [AssetMetadataBulkUpsertDto](doc//AssetMetadataBulkUpsertDto.md)
|
||||
- [AssetMetadataBulkUpsertItemDto](doc//AssetMetadataBulkUpsertItemDto.md)
|
||||
- [AssetMetadataResponseDto](doc//AssetMetadataResponseDto.md)
|
||||
- [AssetMetadataUpsertDto](doc//AssetMetadataUpsertDto.md)
|
||||
- [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md)
|
||||
@@ -387,6 +403,7 @@ Class | Method | HTTP request | Description
|
||||
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
||||
- [CreateLibraryDto](doc//CreateLibraryDto.md)
|
||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||
- [CropParameters](doc//CropParameters.md)
|
||||
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
|
||||
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
|
||||
- [DownloadInfoDto](doc//DownloadInfoDto.md)
|
||||
@@ -431,6 +448,8 @@ Class | Method | HTTP request | Description
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
- [MergePersonDto](doc//MergePersonDto.md)
|
||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||
- [MirrorAxis](doc//MirrorAxis.md)
|
||||
- [MirrorParameters](doc//MirrorParameters.md)
|
||||
- [NotificationCreateDto](doc//NotificationCreateDto.md)
|
||||
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
|
||||
- [NotificationDto](doc//NotificationDto.md)
|
||||
@@ -491,6 +510,7 @@ Class | Method | HTTP request | Description
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||
- [RotateParameters](doc//RotateParameters.md)
|
||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
|
||||
- [SearchExploreItem](doc//SearchExploreItem.md)
|
||||
|
||||
17
mobile/openapi/lib/api.dart
generated
17
mobile/openapi/lib/api.dart
generated
@@ -95,6 +95,13 @@ part 'model/asset_bulk_upload_check_result.dart';
|
||||
part 'model/asset_copy_dto.dart';
|
||||
part 'model/asset_delta_sync_dto.dart';
|
||||
part 'model/asset_delta_sync_response_dto.dart';
|
||||
part 'model/asset_edit_action.dart';
|
||||
part 'model/asset_edit_action_crop.dart';
|
||||
part 'model/asset_edit_action_list_dto.dart';
|
||||
part 'model/asset_edit_action_list_dto_edits_inner.dart';
|
||||
part 'model/asset_edit_action_mirror.dart';
|
||||
part 'model/asset_edit_action_rotate.dart';
|
||||
part 'model/asset_edits_dto.dart';
|
||||
part 'model/asset_face_create_dto.dart';
|
||||
part 'model/asset_face_delete_dto.dart';
|
||||
part 'model/asset_face_response_dto.dart';
|
||||
@@ -109,7 +116,11 @@ part 'model/asset_jobs_dto.dart';
|
||||
part 'model/asset_media_response_dto.dart';
|
||||
part 'model/asset_media_size.dart';
|
||||
part 'model/asset_media_status.dart';
|
||||
part 'model/asset_metadata_key.dart';
|
||||
part 'model/asset_metadata_bulk_delete_dto.dart';
|
||||
part 'model/asset_metadata_bulk_delete_item_dto.dart';
|
||||
part 'model/asset_metadata_bulk_response_dto.dart';
|
||||
part 'model/asset_metadata_bulk_upsert_dto.dart';
|
||||
part 'model/asset_metadata_bulk_upsert_item_dto.dart';
|
||||
part 'model/asset_metadata_response_dto.dart';
|
||||
part 'model/asset_metadata_upsert_dto.dart';
|
||||
part 'model/asset_metadata_upsert_item_dto.dart';
|
||||
@@ -138,6 +149,7 @@ part 'model/contributor_count_response_dto.dart';
|
||||
part 'model/create_album_dto.dart';
|
||||
part 'model/create_library_dto.dart';
|
||||
part 'model/create_profile_image_response_dto.dart';
|
||||
part 'model/crop_parameters.dart';
|
||||
part 'model/database_backup_config.dart';
|
||||
part 'model/download_archive_info.dart';
|
||||
part 'model/download_info_dto.dart';
|
||||
@@ -182,6 +194,8 @@ part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
part 'model/merge_person_dto.dart';
|
||||
part 'model/metadata_search_dto.dart';
|
||||
part 'model/mirror_axis.dart';
|
||||
part 'model/mirror_parameters.dart';
|
||||
part 'model/notification_create_dto.dart';
|
||||
part 'model/notification_delete_all_dto.dart';
|
||||
part 'model/notification_dto.dart';
|
||||
@@ -242,6 +256,7 @@ part 'model/ratings_update.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||
part 'model/rotate_parameters.dart';
|
||||
part 'model/search_album_response_dto.dart';
|
||||
part 'model/search_asset_response_dto.dart';
|
||||
part 'model/search_explore_item.dart';
|
||||
|
||||
334
mobile/openapi/lib/api/assets_api.dart
generated
334
mobile/openapi/lib/api/assets_api.dart
generated
@@ -186,12 +186,12 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetMetadataKey] key (required):
|
||||
Future<Response> deleteAssetMetadataWithHttpInfo(String id, AssetMetadataKey key,) async {
|
||||
/// * [String] key (required):
|
||||
Future<Response> deleteAssetMetadataWithHttpInfo(String id, String key,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/metadata/{key}'
|
||||
.replaceAll('{id}', id)
|
||||
.replaceAll('{key}', key.toString());
|
||||
.replaceAll('{key}', key);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
@@ -222,8 +222,8 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetMetadataKey] key (required):
|
||||
Future<void> deleteAssetMetadata(String id, AssetMetadataKey key,) async {
|
||||
/// * [String] key (required):
|
||||
Future<void> deleteAssetMetadata(String id, String key,) async {
|
||||
final response = await deleteAssetMetadataWithHttpInfo(id, key,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -278,6 +278,54 @@ class AssetsApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete asset metadata
|
||||
///
|
||||
/// Delete metadata key-value pairs for multiple assets.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required):
|
||||
Future<Response> deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/metadata';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = assetMetadataBulkDeleteDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete asset metadata
|
||||
///
|
||||
/// Delete metadata key-value pairs for multiple assets.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required):
|
||||
Future<void> deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async {
|
||||
final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Download original asset
|
||||
///
|
||||
/// Downloads the original file of the specified asset.
|
||||
@@ -288,10 +336,12 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> downloadAssetWithHttpInfo(String id, { String? key, String? slug, }) async {
|
||||
Future<Response> downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/original'
|
||||
.replaceAll('{id}', id);
|
||||
@@ -303,6 +353,9 @@ class AssetsApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (edited != null) {
|
||||
queryParams.addAll(_queryParams('', 'edited', edited));
|
||||
}
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
@@ -332,11 +385,13 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<MultipartFile?> downloadAsset(String id, { String? key, String? slug, }) async {
|
||||
final response = await downloadAssetWithHttpInfo(id, key: key, slug: slug, );
|
||||
Future<MultipartFile?> downloadAsset(String id, { bool? edited, String? key, String? slug, }) async {
|
||||
final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -350,6 +405,67 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Apply edits to an existing asset
|
||||
///
|
||||
/// Apply a series of edit actions (crop, rotate, mirror) to the specified asset.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetEditActionListDto] assetEditActionListDto (required):
|
||||
Future<Response> editAssetWithHttpInfo(String id, AssetEditActionListDto assetEditActionListDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/edits'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = assetEditActionListDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Apply edits to an existing asset
|
||||
///
|
||||
/// Apply a series of edit actions (crop, rotate, mirror) to the specified asset.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetEditActionListDto] assetEditActionListDto (required):
|
||||
Future<AssetEditsDto?> editAsset(String id, AssetEditActionListDto assetEditActionListDto,) async {
|
||||
final response = await editAssetWithHttpInfo(id, assetEditActionListDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsDto',) as AssetEditsDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieve assets by device ID
|
||||
///
|
||||
/// Get all asset of a device that are in the database, ID only.
|
||||
@@ -410,6 +526,63 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieve edits for an existing asset
|
||||
///
|
||||
/// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getAssetEditsWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/edits'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieve edits for an existing asset
|
||||
///
|
||||
/// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<AssetEditsDto?> getAssetEdits(String id,) async {
|
||||
final response = await getAssetEditsWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsDto',) as AssetEditsDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieve an asset
|
||||
///
|
||||
/// Retrieve detailed information about a specific asset.
|
||||
@@ -552,12 +725,12 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetMetadataKey] key (required):
|
||||
Future<Response> getAssetMetadataByKeyWithHttpInfo(String id, AssetMetadataKey key,) async {
|
||||
/// * [String] key (required):
|
||||
Future<Response> getAssetMetadataByKeyWithHttpInfo(String id, String key,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/metadata/{key}'
|
||||
.replaceAll('{id}', id)
|
||||
.replaceAll('{key}', key.toString());
|
||||
.replaceAll('{key}', key);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
@@ -588,8 +761,8 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetMetadataKey] key (required):
|
||||
Future<AssetMetadataResponseDto?> getAssetMetadataByKey(String id, AssetMetadataKey key,) async {
|
||||
/// * [String] key (required):
|
||||
Future<AssetMetadataResponseDto?> getAssetMetadataByKey(String id, String key,) async {
|
||||
final response = await getAssetMetadataByKeyWithHttpInfo(id, key,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -873,6 +1046,55 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Remove edits from an existing asset
|
||||
///
|
||||
/// Removes all edit actions (crop, rotate, mirror) associated with the specified asset.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> removeAssetEditsWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/edits'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove edits from an existing asset
|
||||
///
|
||||
/// Removes all edit actions (crop, rotate, mirror) associated with the specified asset.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> removeAssetEdits(String id,) async {
|
||||
final response = await removeAssetEditsWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace asset
|
||||
///
|
||||
/// Replace the asset with new file, without changing its id.
|
||||
@@ -1228,6 +1450,65 @@ class AssetsApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Upsert asset metadata
|
||||
///
|
||||
/// Upsert metadata key-value pairs for multiple assets.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required):
|
||||
Future<Response> updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/metadata';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = assetMetadataBulkUpsertDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upsert asset metadata
|
||||
///
|
||||
/// Upsert metadata key-value pairs for multiple assets.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required):
|
||||
Future<List<AssetMetadataBulkResponseDto>?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async {
|
||||
final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetMetadataBulkResponseDto>') as List)
|
||||
.cast<AssetMetadataBulkResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Upload asset
|
||||
///
|
||||
/// Uploads a new asset to the server.
|
||||
@@ -1246,8 +1527,6 @@ class AssetsApi {
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
@@ -1263,10 +1542,12 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] livePhotoVideoId:
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||
///
|
||||
/// * [MultipartFile] sidecarData:
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets';
|
||||
|
||||
@@ -1373,8 +1654,6 @@ class AssetsApi {
|
||||
///
|
||||
/// * [DateTime] fileModifiedAt (required):
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
@@ -1390,11 +1669,13 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] livePhotoVideoId:
|
||||
///
|
||||
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||
///
|
||||
/// * [MultipartFile] sidecarData:
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, metadata, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
|
||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -1418,12 +1699,14 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [AssetMediaSize] size:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> viewAssetWithHttpInfo(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
Future<Response> viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/thumbnail'
|
||||
.replaceAll('{id}', id);
|
||||
@@ -1435,6 +1718,9 @@ class AssetsApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (edited != null) {
|
||||
queryParams.addAll(_queryParams('', 'edited', edited));
|
||||
}
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
@@ -1467,13 +1753,15 @@ class AssetsApi {
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] edited:
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [AssetMediaSize] size:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<MultipartFile?> viewAsset(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
final response = await viewAssetWithHttpInfo(id, key: key, size: size, slug: slug, );
|
||||
Future<MultipartFile?> viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async {
|
||||
final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
34
mobile/openapi/lib/api_client.dart
generated
34
mobile/openapi/lib/api_client.dart
generated
@@ -238,6 +238,20 @@ class ApiClient {
|
||||
return AssetDeltaSyncDto.fromJson(value);
|
||||
case 'AssetDeltaSyncResponseDto':
|
||||
return AssetDeltaSyncResponseDto.fromJson(value);
|
||||
case 'AssetEditAction':
|
||||
return AssetEditActionTypeTransformer().decode(value);
|
||||
case 'AssetEditActionCrop':
|
||||
return AssetEditActionCrop.fromJson(value);
|
||||
case 'AssetEditActionListDto':
|
||||
return AssetEditActionListDto.fromJson(value);
|
||||
case 'AssetEditActionListDtoEditsInner':
|
||||
return AssetEditActionListDtoEditsInner.fromJson(value);
|
||||
case 'AssetEditActionMirror':
|
||||
return AssetEditActionMirror.fromJson(value);
|
||||
case 'AssetEditActionRotate':
|
||||
return AssetEditActionRotate.fromJson(value);
|
||||
case 'AssetEditsDto':
|
||||
return AssetEditsDto.fromJson(value);
|
||||
case 'AssetFaceCreateDto':
|
||||
return AssetFaceCreateDto.fromJson(value);
|
||||
case 'AssetFaceDeleteDto':
|
||||
@@ -266,8 +280,16 @@ class ApiClient {
|
||||
return AssetMediaSizeTypeTransformer().decode(value);
|
||||
case 'AssetMediaStatus':
|
||||
return AssetMediaStatusTypeTransformer().decode(value);
|
||||
case 'AssetMetadataKey':
|
||||
return AssetMetadataKeyTypeTransformer().decode(value);
|
||||
case 'AssetMetadataBulkDeleteDto':
|
||||
return AssetMetadataBulkDeleteDto.fromJson(value);
|
||||
case 'AssetMetadataBulkDeleteItemDto':
|
||||
return AssetMetadataBulkDeleteItemDto.fromJson(value);
|
||||
case 'AssetMetadataBulkResponseDto':
|
||||
return AssetMetadataBulkResponseDto.fromJson(value);
|
||||
case 'AssetMetadataBulkUpsertDto':
|
||||
return AssetMetadataBulkUpsertDto.fromJson(value);
|
||||
case 'AssetMetadataBulkUpsertItemDto':
|
||||
return AssetMetadataBulkUpsertItemDto.fromJson(value);
|
||||
case 'AssetMetadataResponseDto':
|
||||
return AssetMetadataResponseDto.fromJson(value);
|
||||
case 'AssetMetadataUpsertDto':
|
||||
@@ -324,6 +346,8 @@ class ApiClient {
|
||||
return CreateLibraryDto.fromJson(value);
|
||||
case 'CreateProfileImageResponseDto':
|
||||
return CreateProfileImageResponseDto.fromJson(value);
|
||||
case 'CropParameters':
|
||||
return CropParameters.fromJson(value);
|
||||
case 'DatabaseBackupConfig':
|
||||
return DatabaseBackupConfig.fromJson(value);
|
||||
case 'DownloadArchiveInfo':
|
||||
@@ -412,6 +436,10 @@ class ApiClient {
|
||||
return MergePersonDto.fromJson(value);
|
||||
case 'MetadataSearchDto':
|
||||
return MetadataSearchDto.fromJson(value);
|
||||
case 'MirrorAxis':
|
||||
return MirrorAxisTypeTransformer().decode(value);
|
||||
case 'MirrorParameters':
|
||||
return MirrorParameters.fromJson(value);
|
||||
case 'NotificationCreateDto':
|
||||
return NotificationCreateDto.fromJson(value);
|
||||
case 'NotificationDeleteAllDto':
|
||||
@@ -532,6 +560,8 @@ class ApiClient {
|
||||
return ReactionTypeTypeTransformer().decode(value);
|
||||
case 'ReverseGeocodingStateResponseDto':
|
||||
return ReverseGeocodingStateResponseDto.fromJson(value);
|
||||
case 'RotateParameters':
|
||||
return RotateParameters.fromJson(value);
|
||||
case 'SearchAlbumResponseDto':
|
||||
return SearchAlbumResponseDto.fromJson(value);
|
||||
case 'SearchAssetResponseDto':
|
||||
|
||||
9
mobile/openapi/lib/api_helper.dart
generated
9
mobile/openapi/lib/api_helper.dart
generated
@@ -58,6 +58,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is AlbumUserRole) {
|
||||
return AlbumUserRoleTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetEditAction) {
|
||||
return AssetEditActionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetJobName) {
|
||||
return AssetJobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
@@ -67,9 +70,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is AssetMediaStatus) {
|
||||
return AssetMediaStatusTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetMetadataKey) {
|
||||
return AssetMetadataKeyTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is AssetOrder) {
|
||||
return AssetOrderTypeTransformer().encode(value).toString();
|
||||
}
|
||||
@@ -112,6 +112,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is MemoryType) {
|
||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is MirrorAxis) {
|
||||
return MirrorAxisTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is NotificationLevel) {
|
||||
return NotificationLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
88
mobile/openapi/lib/model/asset_edit_action.dart
generated
Normal file
88
mobile/openapi/lib/model/asset_edit_action.dart
generated
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class AssetEditAction {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetEditAction._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const crop = AssetEditAction._(r'crop');
|
||||
static const rotate = AssetEditAction._(r'rotate');
|
||||
static const mirror = AssetEditAction._(r'mirror');
|
||||
|
||||
/// List of all possible values in this [enum][AssetEditAction].
|
||||
static const values = <AssetEditAction>[
|
||||
crop,
|
||||
rotate,
|
||||
mirror,
|
||||
];
|
||||
|
||||
static AssetEditAction? fromJson(dynamic value) => AssetEditActionTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetEditAction> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditAction>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditAction.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetEditAction] to String,
|
||||
/// and [decode] dynamic data back to [AssetEditAction].
|
||||
class AssetEditActionTypeTransformer {
|
||||
factory AssetEditActionTypeTransformer() => _instance ??= const AssetEditActionTypeTransformer._();
|
||||
|
||||
const AssetEditActionTypeTransformer._();
|
||||
|
||||
String encode(AssetEditAction data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetEditAction.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
AssetEditAction? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'crop': return AssetEditAction.crop;
|
||||
case r'rotate': return AssetEditAction.rotate;
|
||||
case r'mirror': return AssetEditAction.mirror;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetEditActionTypeTransformer] instance.
|
||||
static AssetEditActionTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/asset_edit_action_crop.dart
generated
Normal file
107
mobile/openapi/lib/model/asset_edit_action_crop.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditActionCrop {
|
||||
/// Returns a new [AssetEditActionCrop] instance.
|
||||
AssetEditActionCrop({
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
AssetEditAction action;
|
||||
|
||||
CropParameters parameters;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionCrop &&
|
||||
other.action == action &&
|
||||
other.parameters == parameters;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(action.hashCode) +
|
||||
(parameters.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditActionCrop[action=$action, parameters=$parameters]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'action'] = this.action;
|
||||
json[r'parameters'] = this.parameters;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditActionCrop] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditActionCrop? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditActionCrop");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditActionCrop(
|
||||
action: AssetEditAction.fromJson(json[r'action'])!,
|
||||
parameters: CropParameters.fromJson(json[r'parameters'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditActionCrop> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditActionCrop>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditActionCrop.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditActionCrop> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditActionCrop>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditActionCrop.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditActionCrop-objects as value to a dart map
|
||||
static Map<String, List<AssetEditActionCrop>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditActionCrop>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditActionCrop.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'action',
|
||||
'parameters',
|
||||
};
|
||||
}
|
||||
|
||||
100
mobile/openapi/lib/model/asset_edit_action_list_dto.dart
generated
Normal file
100
mobile/openapi/lib/model/asset_edit_action_list_dto.dart
generated
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditActionListDto {
|
||||
/// Returns a new [AssetEditActionListDto] instance.
|
||||
AssetEditActionListDto({
|
||||
this.edits = const [],
|
||||
});
|
||||
|
||||
/// list of edits
|
||||
List<AssetEditActionListDtoEditsInner> edits;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDto &&
|
||||
_deepEquality.equals(other.edits, edits);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(edits.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditActionListDto[edits=$edits]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'edits'] = this.edits;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditActionListDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditActionListDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditActionListDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditActionListDto(
|
||||
edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditActionListDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditActionListDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditActionListDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditActionListDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditActionListDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditActionListDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditActionListDto-objects as value to a dart map
|
||||
static Map<String, List<AssetEditActionListDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditActionListDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditActionListDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'edits',
|
||||
};
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart
generated
Normal file
107
mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditActionListDtoEditsInner {
|
||||
/// Returns a new [AssetEditActionListDtoEditsInner] instance.
|
||||
AssetEditActionListDtoEditsInner({
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
AssetEditAction action;
|
||||
|
||||
MirrorParameters parameters;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDtoEditsInner &&
|
||||
other.action == action &&
|
||||
other.parameters == parameters;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(action.hashCode) +
|
||||
(parameters.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditActionListDtoEditsInner[action=$action, parameters=$parameters]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'action'] = this.action;
|
||||
json[r'parameters'] = this.parameters;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditActionListDtoEditsInner] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditActionListDtoEditsInner? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditActionListDtoEditsInner");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditActionListDtoEditsInner(
|
||||
action: AssetEditAction.fromJson(json[r'action'])!,
|
||||
parameters: MirrorParameters.fromJson(json[r'parameters'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditActionListDtoEditsInner> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditActionListDtoEditsInner>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditActionListDtoEditsInner.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditActionListDtoEditsInner> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditActionListDtoEditsInner>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditActionListDtoEditsInner.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditActionListDtoEditsInner-objects as value to a dart map
|
||||
static Map<String, List<AssetEditActionListDtoEditsInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditActionListDtoEditsInner>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditActionListDtoEditsInner.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'action',
|
||||
'parameters',
|
||||
};
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/asset_edit_action_mirror.dart
generated
Normal file
107
mobile/openapi/lib/model/asset_edit_action_mirror.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditActionMirror {
|
||||
/// Returns a new [AssetEditActionMirror] instance.
|
||||
AssetEditActionMirror({
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
AssetEditAction action;
|
||||
|
||||
MirrorParameters parameters;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionMirror &&
|
||||
other.action == action &&
|
||||
other.parameters == parameters;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(action.hashCode) +
|
||||
(parameters.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditActionMirror[action=$action, parameters=$parameters]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'action'] = this.action;
|
||||
json[r'parameters'] = this.parameters;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditActionMirror] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditActionMirror? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditActionMirror");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditActionMirror(
|
||||
action: AssetEditAction.fromJson(json[r'action'])!,
|
||||
parameters: MirrorParameters.fromJson(json[r'parameters'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditActionMirror> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditActionMirror>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditActionMirror.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditActionMirror> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditActionMirror>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditActionMirror.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditActionMirror-objects as value to a dart map
|
||||
static Map<String, List<AssetEditActionMirror>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditActionMirror>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditActionMirror.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'action',
|
||||
'parameters',
|
||||
};
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/asset_edit_action_rotate.dart
generated
Normal file
107
mobile/openapi/lib/model/asset_edit_action_rotate.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditActionRotate {
|
||||
/// Returns a new [AssetEditActionRotate] instance.
|
||||
AssetEditActionRotate({
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
});
|
||||
|
||||
AssetEditAction action;
|
||||
|
||||
RotateParameters parameters;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditActionRotate &&
|
||||
other.action == action &&
|
||||
other.parameters == parameters;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(action.hashCode) +
|
||||
(parameters.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditActionRotate[action=$action, parameters=$parameters]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'action'] = this.action;
|
||||
json[r'parameters'] = this.parameters;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditActionRotate] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditActionRotate? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditActionRotate");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditActionRotate(
|
||||
action: AssetEditAction.fromJson(json[r'action'])!,
|
||||
parameters: RotateParameters.fromJson(json[r'parameters'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditActionRotate> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditActionRotate>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditActionRotate.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditActionRotate> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditActionRotate>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditActionRotate.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditActionRotate-objects as value to a dart map
|
||||
static Map<String, List<AssetEditActionRotate>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditActionRotate>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditActionRotate.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'action',
|
||||
'parameters',
|
||||
};
|
||||
}
|
||||
|
||||
108
mobile/openapi/lib/model/asset_edits_dto.dart
generated
Normal file
108
mobile/openapi/lib/model/asset_edits_dto.dart
generated
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetEditsDto {
|
||||
/// Returns a new [AssetEditsDto] instance.
|
||||
AssetEditsDto({
|
||||
required this.assetId,
|
||||
this.edits = const [],
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
/// list of edits
|
||||
List<AssetEditActionListDtoEditsInner> edits;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetEditsDto &&
|
||||
other.assetId == assetId &&
|
||||
_deepEquality.equals(other.edits, edits);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(edits.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetEditsDto[assetId=$assetId, edits=$edits]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'edits'] = this.edits;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetEditsDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetEditsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetEditsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetEditsDto(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetEditsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetEditsDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetEditsDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetEditsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetEditsDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetEditsDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetEditsDto-objects as value to a dart map
|
||||
static Map<String, List<AssetEditsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetEditsDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetEditsDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'edits',
|
||||
};
|
||||
}
|
||||
|
||||
99
mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetMetadataBulkDeleteDto {
|
||||
/// Returns a new [AssetMetadataBulkDeleteDto] instance.
|
||||
AssetMetadataBulkDeleteDto({
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
List<AssetMetadataBulkDeleteItemDto> items;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkDeleteDto &&
|
||||
_deepEquality.equals(other.items, items);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(items.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetMetadataBulkDeleteDto[items=$items]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'items'] = this.items;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetMetadataBulkDeleteDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMetadataBulkDeleteDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMetadataBulkDeleteDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataBulkDeleteDto(
|
||||
items: AssetMetadataBulkDeleteItemDto.listFromJson(json[r'items']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetMetadataBulkDeleteDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataBulkDeleteDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataBulkDeleteDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetMetadataBulkDeleteDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetMetadataBulkDeleteDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetMetadataBulkDeleteDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetMetadataBulkDeleteDto-objects as value to a dart map
|
||||
static Map<String, List<AssetMetadataBulkDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetMetadataBulkDeleteDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetMetadataBulkDeleteDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'items',
|
||||
};
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart
generated
Normal file
107
mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetMetadataBulkDeleteItemDto {
|
||||
/// Returns a new [AssetMetadataBulkDeleteItemDto] instance.
|
||||
AssetMetadataBulkDeleteItemDto({
|
||||
required this.assetId,
|
||||
required this.key,
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
String key;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkDeleteItemDto &&
|
||||
other.assetId == assetId &&
|
||||
other.key == key;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(key.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetMetadataBulkDeleteItemDto[assetId=$assetId, key=$key]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'key'] = this.key;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetMetadataBulkDeleteItemDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMetadataBulkDeleteItemDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMetadataBulkDeleteItemDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataBulkDeleteItemDto(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetMetadataBulkDeleteItemDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataBulkDeleteItemDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataBulkDeleteItemDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetMetadataBulkDeleteItemDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetMetadataBulkDeleteItemDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetMetadataBulkDeleteItemDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetMetadataBulkDeleteItemDto-objects as value to a dart map
|
||||
static Map<String, List<AssetMetadataBulkDeleteItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetMetadataBulkDeleteItemDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetMetadataBulkDeleteItemDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'key',
|
||||
};
|
||||
}
|
||||
|
||||
123
mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart
generated
Normal file
123
mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart
generated
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetMetadataBulkResponseDto {
|
||||
/// Returns a new [AssetMetadataBulkResponseDto] instance.
|
||||
AssetMetadataBulkResponseDto({
|
||||
required this.assetId,
|
||||
required this.key,
|
||||
required this.updatedAt,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
String key;
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
Object value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto &&
|
||||
other.assetId == assetId &&
|
||||
other.key == key &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.value == value;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(key.hashCode) +
|
||||
(updatedAt.hashCode) +
|
||||
(value.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetMetadataBulkResponseDto[assetId=$assetId, key=$key, updatedAt=$updatedAt, value=$value]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'key'] = this.key;
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'value'] = this.value;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetMetadataBulkResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMetadataBulkResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMetadataBulkResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataBulkResponseDto(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
value: mapValueOfType<Object>(json, r'value')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetMetadataBulkResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataBulkResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataBulkResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetMetadataBulkResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetMetadataBulkResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetMetadataBulkResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetMetadataBulkResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetMetadataBulkResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetMetadataBulkResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetMetadataBulkResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'key',
|
||||
'updatedAt',
|
||||
'value',
|
||||
};
|
||||
}
|
||||
|
||||
99
mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetMetadataBulkUpsertDto {
|
||||
/// Returns a new [AssetMetadataBulkUpsertDto] instance.
|
||||
AssetMetadataBulkUpsertDto({
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
List<AssetMetadataBulkUpsertItemDto> items;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertDto &&
|
||||
_deepEquality.equals(other.items, items);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(items.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetMetadataBulkUpsertDto[items=$items]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'items'] = this.items;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetMetadataBulkUpsertDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMetadataBulkUpsertDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMetadataBulkUpsertDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataBulkUpsertDto(
|
||||
items: AssetMetadataBulkUpsertItemDto.listFromJson(json[r'items']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetMetadataBulkUpsertDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataBulkUpsertDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataBulkUpsertDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetMetadataBulkUpsertDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetMetadataBulkUpsertDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetMetadataBulkUpsertDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetMetadataBulkUpsertDto-objects as value to a dart map
|
||||
static Map<String, List<AssetMetadataBulkUpsertDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetMetadataBulkUpsertDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetMetadataBulkUpsertDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'items',
|
||||
};
|
||||
}
|
||||
|
||||
115
mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetMetadataBulkUpsertItemDto {
|
||||
/// Returns a new [AssetMetadataBulkUpsertItemDto] instance.
|
||||
AssetMetadataBulkUpsertItemDto({
|
||||
required this.assetId,
|
||||
required this.key,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
String key;
|
||||
|
||||
Object value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto &&
|
||||
other.assetId == assetId &&
|
||||
other.key == key &&
|
||||
other.value == value;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(key.hashCode) +
|
||||
(value.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetMetadataBulkUpsertItemDto[assetId=$assetId, key=$key, value=$value]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'key'] = this.key;
|
||||
json[r'value'] = this.value;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetMetadataBulkUpsertItemDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetMetadataBulkUpsertItemDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AssetMetadataBulkUpsertItemDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataBulkUpsertItemDto(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
value: mapValueOfType<Object>(json, r'value')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetMetadataBulkUpsertItemDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataBulkUpsertItemDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataBulkUpsertItemDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetMetadataBulkUpsertItemDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetMetadataBulkUpsertItemDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetMetadataBulkUpsertItemDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetMetadataBulkUpsertItemDto-objects as value to a dart map
|
||||
static Map<String, List<AssetMetadataBulkUpsertItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetMetadataBulkUpsertItemDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetMetadataBulkUpsertItemDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'key',
|
||||
'value',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class AssetMetadataResponseDto {
|
||||
required this.value,
|
||||
});
|
||||
|
||||
AssetMetadataKey key;
|
||||
String key;
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
@@ -57,7 +57,7 @@ class AssetMetadataResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataResponseDto(
|
||||
key: AssetMetadataKey.fromJson(json[r'key'])!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
value: mapValueOfType<Object>(json, r'value')!,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ class AssetMetadataUpsertItemDto {
|
||||
required this.value,
|
||||
});
|
||||
|
||||
AssetMetadataKey key;
|
||||
String key;
|
||||
|
||||
Object value;
|
||||
|
||||
@@ -51,7 +51,7 @@ class AssetMetadataUpsertItemDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetMetadataUpsertItemDto(
|
||||
key: AssetMetadataKey.fromJson(json[r'key'])!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
value: mapValueOfType<Object>(json, r'value')!,
|
||||
);
|
||||
}
|
||||
|
||||
34
mobile/openapi/lib/model/asset_response_dto.dart
generated
34
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -23,6 +23,7 @@ class AssetResponseDto {
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
required this.hasMetadata,
|
||||
required this.height,
|
||||
required this.id,
|
||||
required this.isArchived,
|
||||
required this.isFavorite,
|
||||
@@ -45,6 +46,7 @@ class AssetResponseDto {
|
||||
this.unassignedFaces = const [],
|
||||
required this.updatedAt,
|
||||
required this.visibility,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
/// base64 encoded sha1 hash
|
||||
@@ -77,6 +79,8 @@ class AssetResponseDto {
|
||||
|
||||
bool hasMetadata;
|
||||
|
||||
num? height;
|
||||
|
||||
String id;
|
||||
|
||||
bool isArchived;
|
||||
@@ -141,6 +145,8 @@ class AssetResponseDto {
|
||||
|
||||
AssetVisibility visibility;
|
||||
|
||||
num? width;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||
other.checksum == checksum &&
|
||||
@@ -153,6 +159,7 @@ class AssetResponseDto {
|
||||
other.fileCreatedAt == fileCreatedAt &&
|
||||
other.fileModifiedAt == fileModifiedAt &&
|
||||
other.hasMetadata == hasMetadata &&
|
||||
other.height == height &&
|
||||
other.id == id &&
|
||||
other.isArchived == isArchived &&
|
||||
other.isFavorite == isFavorite &&
|
||||
@@ -174,7 +181,8 @@ class AssetResponseDto {
|
||||
other.type == type &&
|
||||
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.visibility == visibility;
|
||||
other.visibility == visibility &&
|
||||
other.width == width;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -189,6 +197,7 @@ class AssetResponseDto {
|
||||
(fileCreatedAt.hashCode) +
|
||||
(fileModifiedAt.hashCode) +
|
||||
(hasMetadata.hashCode) +
|
||||
(height == null ? 0 : height!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isArchived.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
@@ -210,10 +219,11 @@ class AssetResponseDto {
|
||||
(type.hashCode) +
|
||||
(unassignedFaces.hashCode) +
|
||||
(updatedAt.hashCode) +
|
||||
(visibility.hashCode);
|
||||
(visibility.hashCode) +
|
||||
(width == null ? 0 : width!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -235,6 +245,11 @@ class AssetResponseDto {
|
||||
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
||||
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
||||
json[r'hasMetadata'] = this.hasMetadata;
|
||||
if (this.height != null) {
|
||||
json[r'height'] = this.height;
|
||||
} else {
|
||||
// json[r'height'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isArchived'] = this.isArchived;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
@@ -285,6 +300,11 @@ class AssetResponseDto {
|
||||
json[r'unassignedFaces'] = this.unassignedFaces;
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'visibility'] = this.visibility;
|
||||
if (this.width != null) {
|
||||
json[r'width'] = this.width;
|
||||
} else {
|
||||
// json[r'width'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -307,6 +327,9 @@ class AssetResponseDto {
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
||||
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
||||
height: json[r'height'] == null
|
||||
? null
|
||||
: num.parse('${json[r'height']}'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||
@@ -329,6 +352,9 @@ class AssetResponseDto {
|
||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||
width: json[r'width'] == null
|
||||
? null
|
||||
: num.parse('${json[r'width']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -384,6 +410,7 @@ class AssetResponseDto {
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'hasMetadata',
|
||||
'height',
|
||||
'id',
|
||||
'isArchived',
|
||||
'isFavorite',
|
||||
@@ -397,6 +424,7 @@ class AssetResponseDto {
|
||||
'type',
|
||||
'updatedAt',
|
||||
'visibility',
|
||||
'width',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
135
mobile/openapi/lib/model/crop_parameters.dart
generated
Normal file
135
mobile/openapi/lib/model/crop_parameters.dart
generated
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class CropParameters {
|
||||
/// Returns a new [CropParameters] instance.
|
||||
CropParameters({
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.x,
|
||||
required this.y,
|
||||
});
|
||||
|
||||
/// Height of the crop
|
||||
///
|
||||
/// Minimum value: 1
|
||||
num height;
|
||||
|
||||
/// Width of the crop
|
||||
///
|
||||
/// Minimum value: 1
|
||||
num width;
|
||||
|
||||
/// Top-Left X coordinate of crop
|
||||
///
|
||||
/// Minimum value: 0
|
||||
num x;
|
||||
|
||||
/// Top-Left Y coordinate of crop
|
||||
///
|
||||
/// Minimum value: 0
|
||||
num y;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
|
||||
other.height == height &&
|
||||
other.width == width &&
|
||||
other.x == x &&
|
||||
other.y == y;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(height.hashCode) +
|
||||
(width.hashCode) +
|
||||
(x.hashCode) +
|
||||
(y.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CropParameters[height=$height, width=$width, x=$x, y=$y]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'height'] = this.height;
|
||||
json[r'width'] = this.width;
|
||||
json[r'x'] = this.x;
|
||||
json[r'y'] = this.y;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [CropParameters] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CropParameters? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CropParameters");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return CropParameters(
|
||||
height: num.parse('${json[r'height']}'),
|
||||
width: num.parse('${json[r'width']}'),
|
||||
x: num.parse('${json[r'x']}'),
|
||||
y: num.parse('${json[r'y']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CropParameters> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CropParameters>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CropParameters.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CropParameters> mapFromJson(dynamic json) {
|
||||
final map = <String, CropParameters>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CropParameters.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CropParameters-objects as value to a dart map
|
||||
static Map<String, List<CropParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CropParameters>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = CropParameters.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'height',
|
||||
'width',
|
||||
'x',
|
||||
'y',
|
||||
};
|
||||
}
|
||||
|
||||
3
mobile/openapi/lib/model/job_name.dart
generated
3
mobile/openapi/lib/model/job_name.dart
generated
@@ -29,6 +29,7 @@ class JobName {
|
||||
static const assetDetectFaces = JobName._(r'AssetDetectFaces');
|
||||
static const assetDetectDuplicatesQueueAll = JobName._(r'AssetDetectDuplicatesQueueAll');
|
||||
static const assetDetectDuplicates = JobName._(r'AssetDetectDuplicates');
|
||||
static const assetEditThumbnailGeneration = JobName._(r'AssetEditThumbnailGeneration');
|
||||
static const assetEncodeVideoQueueAll = JobName._(r'AssetEncodeVideoQueueAll');
|
||||
static const assetEncodeVideo = JobName._(r'AssetEncodeVideo');
|
||||
static const assetEmptyTrash = JobName._(r'AssetEmptyTrash');
|
||||
@@ -87,6 +88,7 @@ class JobName {
|
||||
assetDetectFaces,
|
||||
assetDetectDuplicatesQueueAll,
|
||||
assetDetectDuplicates,
|
||||
assetEditThumbnailGeneration,
|
||||
assetEncodeVideoQueueAll,
|
||||
assetEncodeVideo,
|
||||
assetEmptyTrash,
|
||||
@@ -180,6 +182,7 @@ class JobNameTypeTransformer {
|
||||
case r'AssetDetectFaces': return JobName.assetDetectFaces;
|
||||
case r'AssetDetectDuplicatesQueueAll': return JobName.assetDetectDuplicatesQueueAll;
|
||||
case r'AssetDetectDuplicates': return JobName.assetDetectDuplicates;
|
||||
case r'AssetEditThumbnailGeneration': return JobName.assetEditThumbnailGeneration;
|
||||
case r'AssetEncodeVideoQueueAll': return JobName.assetEncodeVideoQueueAll;
|
||||
case r'AssetEncodeVideo': return JobName.assetEncodeVideo;
|
||||
case r'AssetEmptyTrash': return JobName.assetEmptyTrash;
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class AssetMetadataKey {
|
||||
/// Axis to mirror along
|
||||
class MirrorAxis {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetMetadataKey._(this.value);
|
||||
const MirrorAxis._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
@@ -23,20 +23,22 @@ class AssetMetadataKey {
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const mobileApp = AssetMetadataKey._(r'mobile-app');
|
||||
static const horizontal = MirrorAxis._(r'horizontal');
|
||||
static const vertical = MirrorAxis._(r'vertical');
|
||||
|
||||
/// List of all possible values in this [enum][AssetMetadataKey].
|
||||
static const values = <AssetMetadataKey>[
|
||||
mobileApp,
|
||||
/// List of all possible values in this [enum][MirrorAxis].
|
||||
static const values = <MirrorAxis>[
|
||||
horizontal,
|
||||
vertical,
|
||||
];
|
||||
|
||||
static AssetMetadataKey? fromJson(dynamic value) => AssetMetadataKeyTypeTransformer().decode(value);
|
||||
static MirrorAxis? fromJson(dynamic value) => MirrorAxisTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetMetadataKey> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetMetadataKey>[];
|
||||
static List<MirrorAxis> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MirrorAxis>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetMetadataKey.fromJson(row);
|
||||
final value = MirrorAxis.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -46,16 +48,16 @@ class AssetMetadataKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetMetadataKey] to String,
|
||||
/// and [decode] dynamic data back to [AssetMetadataKey].
|
||||
class AssetMetadataKeyTypeTransformer {
|
||||
factory AssetMetadataKeyTypeTransformer() => _instance ??= const AssetMetadataKeyTypeTransformer._();
|
||||
/// Transformation class that can [encode] an instance of [MirrorAxis] to String,
|
||||
/// and [decode] dynamic data back to [MirrorAxis].
|
||||
class MirrorAxisTypeTransformer {
|
||||
factory MirrorAxisTypeTransformer() => _instance ??= const MirrorAxisTypeTransformer._();
|
||||
|
||||
const AssetMetadataKeyTypeTransformer._();
|
||||
const MirrorAxisTypeTransformer._();
|
||||
|
||||
String encode(AssetMetadataKey data) => data.value;
|
||||
String encode(MirrorAxis data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetMetadataKey.
|
||||
/// Decodes a [dynamic value][data] to a MirrorAxis.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
@@ -63,10 +65,11 @@ class AssetMetadataKeyTypeTransformer {
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
AssetMetadataKey? decode(dynamic data, {bool allowNull = true}) {
|
||||
MirrorAxis? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'mobile-app': return AssetMetadataKey.mobileApp;
|
||||
case r'horizontal': return MirrorAxis.horizontal;
|
||||
case r'vertical': return MirrorAxis.vertical;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
@@ -76,7 +79,7 @@ class AssetMetadataKeyTypeTransformer {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetMetadataKeyTypeTransformer] instance.
|
||||
static AssetMetadataKeyTypeTransformer? _instance;
|
||||
/// Singleton [MirrorAxisTypeTransformer] instance.
|
||||
static MirrorAxisTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
100
mobile/openapi/lib/model/mirror_parameters.dart
generated
Normal file
100
mobile/openapi/lib/model/mirror_parameters.dart
generated
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MirrorParameters {
|
||||
/// Returns a new [MirrorParameters] instance.
|
||||
MirrorParameters({
|
||||
required this.axis,
|
||||
});
|
||||
|
||||
/// Axis to mirror along
|
||||
MirrorAxis axis;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MirrorParameters &&
|
||||
other.axis == axis;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(axis.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MirrorParameters[axis=$axis]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'axis'] = this.axis;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MirrorParameters] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MirrorParameters? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MirrorParameters");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MirrorParameters(
|
||||
axis: MirrorAxis.fromJson(json[r'axis'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MirrorParameters> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MirrorParameters>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MirrorParameters.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MirrorParameters> mapFromJson(dynamic json) {
|
||||
final map = <String, MirrorParameters>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MirrorParameters.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MirrorParameters-objects as value to a dart map
|
||||
static Map<String, List<MirrorParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MirrorParameters>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MirrorParameters.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'axis',
|
||||
};
|
||||
}
|
||||
|
||||
12
mobile/openapi/lib/model/permission.dart
generated
12
mobile/openapi/lib/model/permission.dart
generated
@@ -43,6 +43,10 @@ class Permission {
|
||||
static const assetPeriodUpload = Permission._(r'asset.upload');
|
||||
static const assetPeriodReplace = Permission._(r'asset.replace');
|
||||
static const assetPeriodCopy = Permission._(r'asset.copy');
|
||||
static const assetPeriodDerive = Permission._(r'asset.derive');
|
||||
static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get');
|
||||
static const assetPeriodEditPeriodCreate = Permission._(r'asset.edit.create');
|
||||
static const assetPeriodEditPeriodDelete = Permission._(r'asset.edit.delete');
|
||||
static const albumPeriodCreate = Permission._(r'album.create');
|
||||
static const albumPeriodRead = Permission._(r'album.read');
|
||||
static const albumPeriodUpdate = Permission._(r'album.update');
|
||||
@@ -191,6 +195,10 @@ class Permission {
|
||||
assetPeriodUpload,
|
||||
assetPeriodReplace,
|
||||
assetPeriodCopy,
|
||||
assetPeriodDerive,
|
||||
assetPeriodEditPeriodGet,
|
||||
assetPeriodEditPeriodCreate,
|
||||
assetPeriodEditPeriodDelete,
|
||||
albumPeriodCreate,
|
||||
albumPeriodRead,
|
||||
albumPeriodUpdate,
|
||||
@@ -374,6 +382,10 @@ class PermissionTypeTransformer {
|
||||
case r'asset.upload': return Permission.assetPeriodUpload;
|
||||
case r'asset.replace': return Permission.assetPeriodReplace;
|
||||
case r'asset.copy': return Permission.assetPeriodCopy;
|
||||
case r'asset.derive': return Permission.assetPeriodDerive;
|
||||
case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet;
|
||||
case r'asset.edit.create': return Permission.assetPeriodEditPeriodCreate;
|
||||
case r'asset.edit.delete': return Permission.assetPeriodEditPeriodDelete;
|
||||
case r'album.create': return Permission.albumPeriodCreate;
|
||||
case r'album.read': return Permission.albumPeriodRead;
|
||||
case r'album.update': return Permission.albumPeriodUpdate;
|
||||
|
||||
3
mobile/openapi/lib/model/queue_name.dart
generated
3
mobile/openapi/lib/model/queue_name.dart
generated
@@ -40,6 +40,7 @@ class QueueName {
|
||||
static const backupDatabase = QueueName._(r'backupDatabase');
|
||||
static const ocr = QueueName._(r'ocr');
|
||||
static const workflow = QueueName._(r'workflow');
|
||||
static const editor = QueueName._(r'editor');
|
||||
|
||||
/// List of all possible values in this [enum][QueueName].
|
||||
static const values = <QueueName>[
|
||||
@@ -60,6 +61,7 @@ class QueueName {
|
||||
backupDatabase,
|
||||
ocr,
|
||||
workflow,
|
||||
editor,
|
||||
];
|
||||
|
||||
static QueueName? fromJson(dynamic value) => QueueNameTypeTransformer().decode(value);
|
||||
@@ -115,6 +117,7 @@ class QueueNameTypeTransformer {
|
||||
case r'backupDatabase': return QueueName.backupDatabase;
|
||||
case r'ocr': return QueueName.ocr;
|
||||
case r'workflow': return QueueName.workflow;
|
||||
case r'editor': return QueueName.editor;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
@@ -16,6 +16,7 @@ class QueuesResponseLegacyDto {
|
||||
required this.backgroundTask,
|
||||
required this.backupDatabase,
|
||||
required this.duplicateDetection,
|
||||
required this.editor,
|
||||
required this.faceDetection,
|
||||
required this.facialRecognition,
|
||||
required this.library_,
|
||||
@@ -38,6 +39,8 @@ class QueuesResponseLegacyDto {
|
||||
|
||||
QueueResponseLegacyDto duplicateDetection;
|
||||
|
||||
QueueResponseLegacyDto editor;
|
||||
|
||||
QueueResponseLegacyDto faceDetection;
|
||||
|
||||
QueueResponseLegacyDto facialRecognition;
|
||||
@@ -71,6 +74,7 @@ class QueuesResponseLegacyDto {
|
||||
other.backgroundTask == backgroundTask &&
|
||||
other.backupDatabase == backupDatabase &&
|
||||
other.duplicateDetection == duplicateDetection &&
|
||||
other.editor == editor &&
|
||||
other.faceDetection == faceDetection &&
|
||||
other.facialRecognition == facialRecognition &&
|
||||
other.library_ == library_ &&
|
||||
@@ -92,6 +96,7 @@ class QueuesResponseLegacyDto {
|
||||
(backgroundTask.hashCode) +
|
||||
(backupDatabase.hashCode) +
|
||||
(duplicateDetection.hashCode) +
|
||||
(editor.hashCode) +
|
||||
(faceDetection.hashCode) +
|
||||
(facialRecognition.hashCode) +
|
||||
(library_.hashCode) +
|
||||
@@ -108,13 +113,14 @@ class QueuesResponseLegacyDto {
|
||||
(workflow.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, editor=$editor, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'backgroundTask'] = this.backgroundTask;
|
||||
json[r'backupDatabase'] = this.backupDatabase;
|
||||
json[r'duplicateDetection'] = this.duplicateDetection;
|
||||
json[r'editor'] = this.editor;
|
||||
json[r'faceDetection'] = this.faceDetection;
|
||||
json[r'facialRecognition'] = this.facialRecognition;
|
||||
json[r'library'] = this.library_;
|
||||
@@ -144,6 +150,7 @@ class QueuesResponseLegacyDto {
|
||||
backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!,
|
||||
backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!,
|
||||
duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!,
|
||||
editor: QueueResponseLegacyDto.fromJson(json[r'editor'])!,
|
||||
faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!,
|
||||
facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!,
|
||||
library_: QueueResponseLegacyDto.fromJson(json[r'library'])!,
|
||||
@@ -208,6 +215,7 @@ class QueuesResponseLegacyDto {
|
||||
'backgroundTask',
|
||||
'backupDatabase',
|
||||
'duplicateDetection',
|
||||
'editor',
|
||||
'faceDetection',
|
||||
'facialRecognition',
|
||||
'library',
|
||||
|
||||
100
mobile/openapi/lib/model/rotate_parameters.dart
generated
Normal file
100
mobile/openapi/lib/model/rotate_parameters.dart
generated
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class RotateParameters {
|
||||
/// Returns a new [RotateParameters] instance.
|
||||
RotateParameters({
|
||||
required this.angle,
|
||||
});
|
||||
|
||||
/// Rotation angle in degrees
|
||||
num angle;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is RotateParameters &&
|
||||
other.angle == angle;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(angle.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'RotateParameters[angle=$angle]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'angle'] = this.angle;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [RotateParameters] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static RotateParameters? fromJson(dynamic value) {
|
||||
upgradeDto(value, "RotateParameters");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return RotateParameters(
|
||||
angle: num.parse('${json[r'angle']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<RotateParameters> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <RotateParameters>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = RotateParameters.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, RotateParameters> mapFromJson(dynamic json) {
|
||||
final map = <String, RotateParameters>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = RotateParameters.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of RotateParameters-objects as value to a dart map
|
||||
static Map<String, List<RotateParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<RotateParameters>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = RotateParameters.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'angle',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class SyncAssetMetadataDeleteV1 {
|
||||
|
||||
String assetId;
|
||||
|
||||
AssetMetadataKey key;
|
||||
String key;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataDeleteV1 &&
|
||||
@@ -52,7 +52,7 @@ class SyncAssetMetadataDeleteV1 {
|
||||
|
||||
return SyncAssetMetadataDeleteV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
key: AssetMetadataKey.fromJson(json[r'key'])!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -20,7 +20,7 @@ class SyncAssetMetadataV1 {
|
||||
|
||||
String assetId;
|
||||
|
||||
AssetMetadataKey key;
|
||||
String key;
|
||||
|
||||
Object value;
|
||||
|
||||
@@ -58,7 +58,7 @@ class SyncAssetMetadataV1 {
|
||||
|
||||
return SyncAssetMetadataV1(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
key: AssetMetadataKey.fromJson(json[r'key'])!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
value: mapValueOfType<Object>(json, r'value')!,
|
||||
);
|
||||
}
|
||||
|
||||
30
mobile/openapi/lib/model/sync_asset_v1.dart
generated
30
mobile/openapi/lib/model/sync_asset_v1.dart
generated
@@ -18,6 +18,7 @@ class SyncAssetV1 {
|
||||
required this.duration,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
required this.height,
|
||||
required this.id,
|
||||
required this.isFavorite,
|
||||
required this.libraryId,
|
||||
@@ -29,6 +30,7 @@ class SyncAssetV1 {
|
||||
required this.thumbhash,
|
||||
required this.type,
|
||||
required this.visibility,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
String checksum;
|
||||
@@ -41,6 +43,8 @@ class SyncAssetV1 {
|
||||
|
||||
DateTime? fileModifiedAt;
|
||||
|
||||
int? height;
|
||||
|
||||
String id;
|
||||
|
||||
bool isFavorite;
|
||||
@@ -63,6 +67,8 @@ class SyncAssetV1 {
|
||||
|
||||
AssetVisibility visibility;
|
||||
|
||||
int? width;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
|
||||
other.checksum == checksum &&
|
||||
@@ -70,6 +76,7 @@ class SyncAssetV1 {
|
||||
other.duration == duration &&
|
||||
other.fileCreatedAt == fileCreatedAt &&
|
||||
other.fileModifiedAt == fileModifiedAt &&
|
||||
other.height == height &&
|
||||
other.id == id &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.libraryId == libraryId &&
|
||||
@@ -80,7 +87,8 @@ class SyncAssetV1 {
|
||||
other.stackId == stackId &&
|
||||
other.thumbhash == thumbhash &&
|
||||
other.type == type &&
|
||||
other.visibility == visibility;
|
||||
other.visibility == visibility &&
|
||||
other.width == width;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -90,6 +98,7 @@ class SyncAssetV1 {
|
||||
(duration == null ? 0 : duration!.hashCode) +
|
||||
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||
(height == null ? 0 : height!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||
@@ -100,10 +109,11 @@ class SyncAssetV1 {
|
||||
(stackId == null ? 0 : stackId!.hashCode) +
|
||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||
(type.hashCode) +
|
||||
(visibility.hashCode);
|
||||
(visibility.hashCode) +
|
||||
(width == null ? 0 : width!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
|
||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -127,6 +137,11 @@ class SyncAssetV1 {
|
||||
json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'fileModifiedAt'] = null;
|
||||
}
|
||||
if (this.height != null) {
|
||||
json[r'height'] = this.height;
|
||||
} else {
|
||||
// json[r'height'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
@@ -159,6 +174,11 @@ class SyncAssetV1 {
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
json[r'visibility'] = this.visibility;
|
||||
if (this.width != null) {
|
||||
json[r'width'] = this.width;
|
||||
} else {
|
||||
// json[r'width'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -176,6 +196,7 @@ class SyncAssetV1 {
|
||||
duration: mapValueOfType<String>(json, r'duration'),
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||
height: mapValueOfType<int>(json, r'height'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
@@ -187,6 +208,7 @@ class SyncAssetV1 {
|
||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||
width: mapValueOfType<int>(json, r'width'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -239,6 +261,7 @@ class SyncAssetV1 {
|
||||
'duration',
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'height',
|
||||
'id',
|
||||
'isFavorite',
|
||||
'libraryId',
|
||||
@@ -250,6 +273,7 @@ class SyncAssetV1 {
|
||||
'thumbhash',
|
||||
'type',
|
||||
'visibility',
|
||||
'width',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
10
mobile/openapi/lib/model/system_config_job_dto.dart
generated
10
mobile/openapi/lib/model/system_config_job_dto.dart
generated
@@ -14,6 +14,7 @@ class SystemConfigJobDto {
|
||||
/// Returns a new [SystemConfigJobDto] instance.
|
||||
SystemConfigJobDto({
|
||||
required this.backgroundTask,
|
||||
required this.editor,
|
||||
required this.faceDetection,
|
||||
required this.library_,
|
||||
required this.metadataExtraction,
|
||||
@@ -30,6 +31,8 @@ class SystemConfigJobDto {
|
||||
|
||||
JobSettingsDto backgroundTask;
|
||||
|
||||
JobSettingsDto editor;
|
||||
|
||||
JobSettingsDto faceDetection;
|
||||
|
||||
JobSettingsDto library_;
|
||||
@@ -57,6 +60,7 @@ class SystemConfigJobDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigJobDto &&
|
||||
other.backgroundTask == backgroundTask &&
|
||||
other.editor == editor &&
|
||||
other.faceDetection == faceDetection &&
|
||||
other.library_ == library_ &&
|
||||
other.metadataExtraction == metadataExtraction &&
|
||||
@@ -74,6 +78,7 @@ class SystemConfigJobDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(backgroundTask.hashCode) +
|
||||
(editor.hashCode) +
|
||||
(faceDetection.hashCode) +
|
||||
(library_.hashCode) +
|
||||
(metadataExtraction.hashCode) +
|
||||
@@ -88,11 +93,12 @@ class SystemConfigJobDto {
|
||||
(workflow.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, editor=$editor, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'backgroundTask'] = this.backgroundTask;
|
||||
json[r'editor'] = this.editor;
|
||||
json[r'faceDetection'] = this.faceDetection;
|
||||
json[r'library'] = this.library_;
|
||||
json[r'metadataExtraction'] = this.metadataExtraction;
|
||||
@@ -118,6 +124,7 @@ class SystemConfigJobDto {
|
||||
|
||||
return SystemConfigJobDto(
|
||||
backgroundTask: JobSettingsDto.fromJson(json[r'backgroundTask'])!,
|
||||
editor: JobSettingsDto.fromJson(json[r'editor'])!,
|
||||
faceDetection: JobSettingsDto.fromJson(json[r'faceDetection'])!,
|
||||
library_: JobSettingsDto.fromJson(json[r'library'])!,
|
||||
metadataExtraction: JobSettingsDto.fromJson(json[r'metadataExtraction'])!,
|
||||
@@ -178,6 +185,7 @@ class SystemConfigJobDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'backgroundTask',
|
||||
'editor',
|
||||
'faceDetection',
|
||||
'library',
|
||||
'metadataExtraction',
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export 'src/buttons/close_button.dart';
|
||||
export 'src/buttons/icon_button.dart';
|
||||
export 'src/components/close_button.dart';
|
||||
export 'src/components/form.dart';
|
||||
export 'src/components/icon_button.dart';
|
||||
export 'src/components/password_input.dart';
|
||||
export 'src/components/text_button.dart';
|
||||
export 'src/components/text_input.dart';
|
||||
export 'src/constants.dart';
|
||||
export 'src/theme.dart';
|
||||
export 'src/translation.dart';
|
||||
export 'src/types.dart';
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/buttons/icon_button.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
import 'icon_button.dart';
|
||||
|
||||
class ImmichCloseButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.ghost,
|
||||
});
|
||||
@@ -20,6 +21,6 @@ class ImmichCloseButton extends StatelessWidget {
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onTap: onTap ?? () => Navigator.of(context).pop(),
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
98
mobile/packages/ui/lib/src/components/form.dart
Normal file
98
mobile/packages/ui/lib/src/components/form.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichForm extends StatefulWidget {
|
||||
final String? submitText;
|
||||
final IconData? submitIcon;
|
||||
final FutureOr<void> Function()? onSubmit;
|
||||
final Widget child;
|
||||
|
||||
const ImmichForm({
|
||||
super.key,
|
||||
this.submitText,
|
||||
this.submitIcon,
|
||||
required this.onSubmit,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichForm> createState() => ImmichFormState();
|
||||
|
||||
static ImmichFormState of(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<_ImmichFormScope>();
|
||||
if (scope == null) {
|
||||
throw FlutterError(
|
||||
'ImmichForm.of() called with a context that does not contain an ImmichForm.\n'
|
||||
'No ImmichForm ancestor could be found starting from the context that was passed to '
|
||||
'ImmichForm.of(). This usually happens when the context provided is '
|
||||
'from a widget above the ImmichForm.\n'
|
||||
'The context used was:\n'
|
||||
'$context',
|
||||
);
|
||||
}
|
||||
return scope._formState;
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichFormState extends State<ImmichForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
|
||||
FutureOr<void> submit() async {
|
||||
final isValid = _formKey.currentState?.validate() ?? false;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await widget.onSubmit?.call();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final submitText = widget.submitText ?? context.translations.submit;
|
||||
return _ImmichFormScope(
|
||||
formState: this,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
spacing: ImmichSpacing.md,
|
||||
children: [
|
||||
widget.child,
|
||||
ImmichTextButton(
|
||||
labelText: submitText,
|
||||
icon: widget.submitIcon,
|
||||
variant: ImmichVariant.filled,
|
||||
loading: _isLoading,
|
||||
onPressed: submit,
|
||||
disabled: widget.onSubmit == null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImmichFormScope extends InheritedWidget {
|
||||
const _ImmichFormScope({required super.child, required ImmichFormState formState}) : _formState = formState;
|
||||
|
||||
final ImmichFormState _formState;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ImmichFormScope oldWidget) => oldWidget._formState != _formState;
|
||||
}
|
||||
@@ -3,42 +3,48 @@ import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.onPrimary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary,
|
||||
ImmichColor.primary => colorScheme.onPrimary,
|
||||
ImmichColor.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
final effectiveOnPressed = disabled ? null : onPressed;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: onTap,
|
||||
onPressed: effectiveOnPressed,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
58
mobile/packages/ui/lib/src/components/password_input.dart
Normal file
58
mobile/packages/ui/lib/src/components/password_input.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/components/text_input.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichPasswordInput extends StatefulWidget {
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(BuildContext, String)? onSubmit;
|
||||
final TextInputAction? keyboardAction;
|
||||
|
||||
const ImmichPasswordInput({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.label,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
this.onSubmit,
|
||||
this.keyboardAction,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _ImmichPasswordInputState();
|
||||
}
|
||||
|
||||
class _ImmichPasswordInputState extends State<ImmichPasswordInput> {
|
||||
bool _visible = false;
|
||||
|
||||
void _toggleVisibility() {
|
||||
setState(() {
|
||||
_visible = !_visible;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichTextInput(
|
||||
key: widget.key,
|
||||
label: widget.label ?? context.translations.password,
|
||||
hintText: widget.hintText,
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
validator: widget.validator,
|
||||
onSubmit: widget.onSubmit,
|
||||
keyboardAction: widget.keyboardAction,
|
||||
obscureText: !_visible,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _toggleVisibility,
|
||||
icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded),
|
||||
),
|
||||
autofillHints: [AutofillHints.password],
|
||||
keyboardType: TextInputType.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
87
mobile/packages/ui/lib/src/components/text_button.dart
Normal file
87
mobile/packages/ui/lib/src/components/text_button.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichTextButton extends StatelessWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool expanded;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichTextButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.color = ImmichColor.primary,
|
||||
this.expanded = true,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
Widget _buildButton(ImmichVariant variant) {
|
||||
final Widget? effectiveIcon = loading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: icon != null
|
||||
? Icon(icon, fontWeight: FontWeight.w600)
|
||||
: null;
|
||||
final hasIcon = effectiveIcon != null;
|
||||
|
||||
final label = Text(labelText, style: const TextStyle(fontSize: ImmichTextSize.body, fontWeight: FontWeight.bold));
|
||||
final style = ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: ImmichSpacing.md));
|
||||
|
||||
final effectiveOnPressed = disabled || loading ? null : onPressed;
|
||||
|
||||
switch (variant) {
|
||||
case ImmichVariant.filled:
|
||||
if (hasIcon) {
|
||||
return ElevatedButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
case ImmichVariant.ghost:
|
||||
if (hasIcon) {
|
||||
return TextButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final button = _buildButton(variant);
|
||||
if (expanded) {
|
||||
return SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
}
|
||||
88
mobile/packages/ui/lib/src/components/text_input.dart
Normal file
88
mobile/packages/ui/lib/src/components/text_input.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImmichTextInput extends StatefulWidget {
|
||||
final String label;
|
||||
final String? hintText;
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(BuildContext, String)? onSubmit;
|
||||
final TextInputType keyboardType;
|
||||
final TextInputAction? keyboardAction;
|
||||
final List<String>? autofillHints;
|
||||
final Widget? suffixIcon;
|
||||
final bool obscureText;
|
||||
|
||||
const ImmichTextInput({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
required this.label,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
this.onSubmit,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.keyboardAction,
|
||||
this.autofillHints,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _ImmichTextInputState();
|
||||
}
|
||||
|
||||
class _ImmichTextInputState extends State<ImmichTextInput> {
|
||||
late final FocusNode _focusNode;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.focusNode == null) {
|
||||
_focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _validateInput(String? value) {
|
||||
setState(() {
|
||||
_error = widget.validator?.call(value);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get _hasError => _error != null && _error!.isNotEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = Theme.of(context);
|
||||
|
||||
return TextFormField(
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
labelText: widget.label,
|
||||
labelStyle: themeData.inputDecorationTheme.labelStyle?.copyWith(
|
||||
color: _hasError ? themeData.colorScheme.error : null,
|
||||
),
|
||||
errorText: _error,
|
||||
suffixIcon: widget.suffixIcon,
|
||||
),
|
||||
obscureText: widget.obscureText,
|
||||
validator: _validateInput,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.keyboardAction,
|
||||
autofillHints: widget.autofillHints,
|
||||
onTap: () => setState(() => _error = null),
|
||||
onTapOutside: (_) => _focusNode.unfocus(),
|
||||
onFieldSubmitted: (value) => widget.onSubmit?.call(context, value),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
mobile/packages/ui/lib/src/constants.dart
Normal file
199
mobile/packages/ui/lib/src/constants.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
/// Spacing constants for gaps between widgets
|
||||
abstract class ImmichSpacing {
|
||||
const ImmichSpacing._();
|
||||
|
||||
/// Extra small spacing: 4.0
|
||||
static const double xs = 4.0;
|
||||
|
||||
/// Small spacing: 8.0
|
||||
static const double sm = 8.0;
|
||||
|
||||
/// Medium spacing (default): 12.0
|
||||
static const double md = 12.0;
|
||||
|
||||
/// Large spacing: 16.0
|
||||
static const double lg = 16.0;
|
||||
|
||||
/// Extra large spacing: 24.0
|
||||
static const double xl = 24.0;
|
||||
|
||||
/// Extra extra large spacing: 32.0
|
||||
static const double xxl = 32.0;
|
||||
|
||||
/// Extra extra extra large spacing: 48.0
|
||||
static const double xxxl = 48.0;
|
||||
}
|
||||
|
||||
/// Border radius constants for consistent rounded corners
|
||||
abstract class ImmichRadius {
|
||||
const ImmichRadius._();
|
||||
|
||||
/// No radius: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Extra small radius: 4.0
|
||||
static const double xs = 4.0;
|
||||
|
||||
/// Small radius: 8.0
|
||||
static const double sm = 8.0;
|
||||
|
||||
/// Medium radius (default): 12.0
|
||||
static const double md = 12.0;
|
||||
|
||||
/// Large radius: 16.0
|
||||
static const double lg = 16.0;
|
||||
|
||||
/// Extra large radius: 20.0
|
||||
static const double xl = 20.0;
|
||||
|
||||
/// Extra extra large radius: 24.0
|
||||
static const double xxl = 24.0;
|
||||
|
||||
/// Full circular radius: infinity
|
||||
static const double full = double.infinity;
|
||||
}
|
||||
|
||||
/// Icon size constants for consistent icon sizing
|
||||
abstract class ImmichIconSize {
|
||||
const ImmichIconSize._();
|
||||
|
||||
/// Extra small icon: 16.0
|
||||
static const double xs = 16.0;
|
||||
|
||||
/// Small icon: 20.0
|
||||
static const double sm = 20.0;
|
||||
|
||||
/// Medium icon (default): 24.0
|
||||
static const double md = 24.0;
|
||||
|
||||
/// Large icon: 32.0
|
||||
static const double lg = 32.0;
|
||||
|
||||
/// Extra large icon: 40.0
|
||||
static const double xl = 40.0;
|
||||
|
||||
/// Extra extra large icon: 48.0
|
||||
static const double xxl = 48.0;
|
||||
}
|
||||
|
||||
/// Animation duration constants for consistent timing
|
||||
abstract class ImmichDuration {
|
||||
const ImmichDuration._();
|
||||
|
||||
/// Extra fast: 100ms
|
||||
static const Duration extraFast = Duration(milliseconds: 100);
|
||||
|
||||
/// Fast: 150ms
|
||||
static const Duration fast = Duration(milliseconds: 150);
|
||||
|
||||
/// Normal: 200ms
|
||||
static const Duration normal = Duration(milliseconds: 200);
|
||||
|
||||
/// Moderate: 300ms
|
||||
static const Duration moderate = Duration(milliseconds: 300);
|
||||
|
||||
/// Slow: 500ms
|
||||
static const Duration slow = Duration(milliseconds: 500);
|
||||
|
||||
/// Extra slow: 700ms
|
||||
static const Duration extraSlow = Duration(milliseconds: 700);
|
||||
}
|
||||
|
||||
/// Elevation constants for consistent shadows and depth
|
||||
abstract class ImmichElevation {
|
||||
const ImmichElevation._();
|
||||
|
||||
/// No elevation: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Extra small elevation: 1.0
|
||||
static const double xs = 1.0;
|
||||
|
||||
/// Small elevation: 2.0
|
||||
static const double sm = 2.0;
|
||||
|
||||
/// Medium elevation: 4.0
|
||||
static const double md = 4.0;
|
||||
|
||||
/// Large elevation: 8.0
|
||||
static const double lg = 8.0;
|
||||
|
||||
/// Extra large elevation: 12.0
|
||||
static const double xl = 12.0;
|
||||
|
||||
/// Extra extra large elevation: 16.0
|
||||
static const double xxl = 16.0;
|
||||
}
|
||||
|
||||
/// Border width constants (similar to Tailwind's border-* scale)
|
||||
abstract class ImmichBorderWidth {
|
||||
const ImmichBorderWidth._();
|
||||
|
||||
/// No border: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Hairline border: 0.5
|
||||
static const double hairline = 0.5;
|
||||
|
||||
/// Default border: 1.0 (border)
|
||||
static const double base = 1.0;
|
||||
|
||||
/// Medium border: 2.0 (border-2)
|
||||
static const double md = 2.0;
|
||||
|
||||
/// Large border: 3.0 (border-4)
|
||||
static const double lg = 3.0;
|
||||
|
||||
/// Extra large border: 4.0
|
||||
static const double xl = 4.0;
|
||||
}
|
||||
|
||||
/// Text size constants with semantic HTML-like naming
|
||||
/// These follow a type scale for harmonious text hierarchy
|
||||
abstract class ImmichTextSize {
|
||||
const ImmichTextSize._();
|
||||
|
||||
/// Caption text: 10.0
|
||||
/// Use for: Tiny labels, legal text, metadata, timestamps
|
||||
static const double caption = 10.0;
|
||||
|
||||
/// Label text: 12.0
|
||||
/// Use for: Form labels, secondary text, helper text
|
||||
static const double label = 12.0;
|
||||
|
||||
/// Body text: 14.0 (default)
|
||||
/// Use for: Main body text, paragraphs, default UI text
|
||||
static const double body = 14.0;
|
||||
|
||||
/// Body emphasized: 16.0
|
||||
/// Use for: Emphasized body text, button labels, tabs
|
||||
static const double bodyLarge = 16.0;
|
||||
|
||||
/// Heading 6: 18.0 (smallest heading)
|
||||
/// Use for: Subtitles, card titles, section headers
|
||||
static const double h6 = 18.0;
|
||||
|
||||
/// Heading 5: 20.0
|
||||
/// Use for: Small headings, prominent labels
|
||||
static const double h5 = 20.0;
|
||||
|
||||
/// Heading 4: 24.0
|
||||
/// Use for: Page titles, dialog titles
|
||||
static const double h4 = 24.0;
|
||||
|
||||
/// Heading 3: 30.0
|
||||
/// Use for: Section headings, large headings
|
||||
static const double h3 = 30.0;
|
||||
|
||||
/// Heading 2: 36.0
|
||||
/// Use for: Major section headings
|
||||
static const double h2 = 36.0;
|
||||
|
||||
/// Heading 1: 48.0 (largest heading)
|
||||
/// Use for: Page hero headings, main titles
|
||||
static const double h1 = 48.0;
|
||||
|
||||
/// Display text: 60.0
|
||||
/// Use for: Hero numbers, splash screens, extra large display
|
||||
static const double display = 60.0;
|
||||
}
|
||||
6
mobile/packages/ui/lib/src/internal.dart
Normal file
6
mobile/packages/ui/lib/src/internal.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/translation.dart';
|
||||
|
||||
extension TranslationHelper on BuildContext {
|
||||
ImmichTranslations get translations => ImmichTranslationProvider.of(this);
|
||||
}
|
||||
42
mobile/packages/ui/lib/src/theme.dart
Normal file
42
mobile/packages/ui/lib/src/theme.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
|
||||
class ImmichThemeProvider extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final Widget child;
|
||||
|
||||
const ImmichThemeProvider({super.key, required this.colorScheme, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: colorScheme,
|
||||
brightness: colorScheme.brightness,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.primary),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.primary),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
labelStyle: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w600),
|
||||
hintStyle: const TextStyle(fontSize: ImmichTextSize.body),
|
||||
errorStyle: TextStyle(color: colorScheme.error, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
31
mobile/packages/ui/lib/src/translation.dart
Normal file
31
mobile/packages/ui/lib/src/translation.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImmichTranslations {
|
||||
late String submit;
|
||||
late String password;
|
||||
|
||||
ImmichTranslations({String? submit, String? password}) {
|
||||
this.submit = submit ?? 'Submit';
|
||||
this.password = password ?? 'Password';
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichTranslationProvider extends InheritedWidget {
|
||||
final ImmichTranslations? translations;
|
||||
|
||||
const ImmichTranslationProvider({
|
||||
super.key,
|
||||
this.translations,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static ImmichTranslations of(BuildContext context) {
|
||||
final provider = context.dependOnInheritedWidgetOfExactType<ImmichTranslationProvider>();
|
||||
return provider?.translations ?? ImmichTranslations();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant ImmichTranslationProvider oldWidget) {
|
||||
return oldWidget.translations != translations;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ version: 2.4.1+3030
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
flutter: 3.38.5
|
||||
flutter: 3.35.7
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.0
|
||||
@@ -54,7 +54,7 @@ dependencies:
|
||||
isar_community_flutter_libs: 3.3.0-dev.3
|
||||
local_auth: ^2.3.0
|
||||
logging: ^1.3.0
|
||||
maplibre_gl: 0.24.1
|
||||
maplibre_gl: ^0.22.0
|
||||
|
||||
native_video_player:
|
||||
git:
|
||||
@@ -81,7 +81,7 @@ dependencies:
|
||||
socket_io_client: ^2.0.3+1
|
||||
stream_transform: ^2.1.1
|
||||
thumbhash: 0.1.0+1
|
||||
timezone: 0.10.1
|
||||
timezone: ^0.9.4
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.3.0
|
||||
@@ -90,7 +90,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^9.0.0
|
||||
build_runner: ^2.4.8
|
||||
custom_lint: 0.8.1
|
||||
custom_lint: ^0.7.5
|
||||
# Drift generator
|
||||
drift_dev: ^2.26.0
|
||||
fake_async: ^1.3.3
|
||||
|
||||
185
mobile/test/domain/repositories/sync_stream_repository_test.dart
Normal file
185
mobile/test/domain/repositories/sync_stream_repository_test.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
SyncUserV1 _createUser({String id = 'user-1'}) {
|
||||
return SyncUserV1(
|
||||
id: id,
|
||||
name: 'Test User',
|
||||
email: 'test@test.com',
|
||||
deletedAt: null,
|
||||
avatarColor: null,
|
||||
hasProfileImage: false,
|
||||
profileChangedAt: DateTime(2024, 1, 1),
|
||||
);
|
||||
}
|
||||
|
||||
SyncAssetV1 _createAsset({
|
||||
required String id,
|
||||
required String checksum,
|
||||
required String fileName,
|
||||
String ownerId = 'user-1',
|
||||
int? width,
|
||||
int? height,
|
||||
}) {
|
||||
return SyncAssetV1(
|
||||
id: id,
|
||||
checksum: checksum,
|
||||
originalFileName: fileName,
|
||||
type: AssetTypeEnum.IMAGE,
|
||||
ownerId: ownerId,
|
||||
isFavorite: false,
|
||||
fileCreatedAt: DateTime(2024, 1, 1),
|
||||
fileModifiedAt: DateTime(2024, 1, 1),
|
||||
localDateTime: DateTime(2024, 1, 1),
|
||||
visibility: AssetVisibility.timeline,
|
||||
width: width,
|
||||
height: height,
|
||||
deletedAt: null,
|
||||
duration: null,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
);
|
||||
}
|
||||
|
||||
SyncAssetExifV1 _createExif({
|
||||
required String assetId,
|
||||
required int width,
|
||||
required int height,
|
||||
required String orientation,
|
||||
}) {
|
||||
return SyncAssetExifV1(
|
||||
assetId: assetId,
|
||||
exifImageWidth: width,
|
||||
exifImageHeight: height,
|
||||
orientation: orientation,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: null,
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
late SyncStreamRepository sut;
|
||||
|
||||
setUp(() async {
|
||||
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
sut = SyncStreamRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('SyncStreamRepository - Dimension swapping based on orientation', () {
|
||||
test('swaps dimensions for asset with rotated orientation', () async {
|
||||
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
|
||||
|
||||
for (final orientation in flippedOrientations) {
|
||||
final assetId = 'asset-$orientation-degrees';
|
||||
|
||||
await sut.updateUsersV1([_createUser()]);
|
||||
|
||||
final asset = _createAsset(
|
||||
id: assetId,
|
||||
checksum: 'checksum-$orientation',
|
||||
fileName: 'rotated_$orientation.jpg',
|
||||
);
|
||||
await sut.updateAssetsV1([asset]);
|
||||
|
||||
final exif = _createExif(
|
||||
assetId: assetId,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
orientation: orientation, // EXIF orientation value for 90 degrees CW
|
||||
);
|
||||
await sut.updateAssetsExifV1([exif]);
|
||||
|
||||
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
expect(result.width, equals(1080));
|
||||
expect(result.height, equals(1920));
|
||||
}
|
||||
});
|
||||
|
||||
test('does not swap dimensions for asset with normal orientation', () async {
|
||||
final nonFlippedOrientations = ['1', '2', '3', '4'];
|
||||
for (final orientation in nonFlippedOrientations) {
|
||||
final assetId = 'asset-$orientation-degrees';
|
||||
|
||||
await sut.updateUsersV1([_createUser()]);
|
||||
|
||||
final asset = _createAsset(id: assetId, checksum: 'checksum-$orientation', fileName: 'normal_$orientation.jpg');
|
||||
await sut.updateAssetsV1([asset]);
|
||||
|
||||
final exif = _createExif(
|
||||
assetId: assetId,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
orientation: orientation, // EXIF orientation value for normal
|
||||
);
|
||||
await sut.updateAssetsExifV1([exif]);
|
||||
|
||||
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
expect(result.width, equals(1920));
|
||||
expect(result.height, equals(1080));
|
||||
}
|
||||
});
|
||||
|
||||
test('does not update dimensions if asset already has width and height', () async {
|
||||
const assetId = 'asset-with-dimensions';
|
||||
const existingWidth = 1920;
|
||||
const existingHeight = 1080;
|
||||
const exifWidth = 3840;
|
||||
const exifHeight = 2160;
|
||||
|
||||
await sut.updateUsersV1([_createUser()]);
|
||||
|
||||
final asset = _createAsset(
|
||||
id: assetId,
|
||||
checksum: 'checksum-with-dims',
|
||||
fileName: 'with_dimensions.jpg',
|
||||
width: existingWidth,
|
||||
height: existingHeight,
|
||||
);
|
||||
await sut.updateAssetsV1([asset]);
|
||||
|
||||
final exif = _createExif(assetId: assetId, width: exifWidth, height: exifHeight, orientation: '6');
|
||||
await sut.updateAssetsExifV1([exif]);
|
||||
|
||||
// Verify the asset still has original dimensions (not updated from EXIF)
|
||||
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||
final result = await query.getSingle();
|
||||
|
||||
expect(result.width, equals(existingWidth), reason: 'Width should remain as originally set');
|
||||
expect(result.height, equals(existingHeight), reason: 'Height should remain as originally set');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -18,30 +18,34 @@ void main() {
|
||||
mockAlbumApiRepo = MockDriftAlbumApiRepository();
|
||||
sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo);
|
||||
|
||||
when(() => mockRemoteAlbumRepo.getNewestAssetTimestamp(any())).thenAnswer((invocation) {
|
||||
// Simulate a timestamp for the newest asset in the album
|
||||
final albumID = invocation.positionalArguments[0] as String;
|
||||
|
||||
if (albumID == '1') {
|
||||
return Future.value(DateTime(2023, 1, 1));
|
||||
} else if (albumID == '2') {
|
||||
return Future.value(DateTime(2023, 2, 1));
|
||||
when(() => mockRemoteAlbumRepo.getNewestAssetTimestampForAlbums(any())).thenAnswer((invocation) async {
|
||||
final albumIds = invocation.positionalArguments[0] as List<String>;
|
||||
final result = <String, DateTime?>{};
|
||||
for (final id in albumIds) {
|
||||
if (id == '1') {
|
||||
result[id] = DateTime(2023, 1, 1);
|
||||
} else if (id == '2') {
|
||||
result[id] = DateTime(2023, 2, 1);
|
||||
} else {
|
||||
result[id] = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
||||
return result;
|
||||
});
|
||||
|
||||
when(() => mockRemoteAlbumRepo.getOldestAssetTimestamp(any())).thenAnswer((invocation) {
|
||||
// Simulate a timestamp for the oldest asset in the album
|
||||
final albumID = invocation.positionalArguments[0] as String;
|
||||
|
||||
if (albumID == '1') {
|
||||
return Future.value(DateTime(2019, 1, 1));
|
||||
} else if (albumID == '2') {
|
||||
return Future.value(DateTime(2019, 2, 1));
|
||||
when(() => mockRemoteAlbumRepo.getOldestAssetTimestampForAlbums(any())).thenAnswer((invocation) async {
|
||||
final albumIds = invocation.positionalArguments[0] as List<String>;
|
||||
final result = <String, DateTime?>{};
|
||||
for (final id in albumIds) {
|
||||
if (id == '1') {
|
||||
result[id] = DateTime(2019, 1, 1);
|
||||
} else if (id == '2') {
|
||||
result[id] = DateTime(2019, 2, 1);
|
||||
} else {
|
||||
result[id] = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -166,8 +166,8 @@ void main() {
|
||||
expect(result, 1080 / 1920);
|
||||
});
|
||||
|
||||
test('handles various flipped EXIF orientations correctly', () async {
|
||||
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
|
||||
test('should not flip remote asset dimensions', () async {
|
||||
final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90'];
|
||||
|
||||
for (final orientation in flippedOrientations) {
|
||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
|
||||
@@ -178,23 +178,7 @@ void main() {
|
||||
|
||||
final result = await sut.getAspectRatio(remoteAsset);
|
||||
|
||||
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions');
|
||||
}
|
||||
});
|
||||
|
||||
test('handles various non-flipped EXIF orientations correctly', () async {
|
||||
final nonFlippedOrientations = ['1', '2', '3', '4'];
|
||||
|
||||
for (final orientation in nonFlippedOrientations) {
|
||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
|
||||
|
||||
final exif = ExifInfo(orientation: orientation);
|
||||
|
||||
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
|
||||
|
||||
final result = await sut.getAspectRatio(remoteAsset);
|
||||
|
||||
expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions');
|
||||
expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user