mirror of
https://github.com/immich-app/immich.git
synced 2026-01-14 14:06:41 -08:00
Compare commits
105 Commits
fix/ios-ha
...
fix/merged
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b76ec1be | ||
|
|
a303be5358 | ||
|
|
cbbf7683ee | ||
|
|
e7cc6fed36 | ||
|
|
ecec9a8151 | ||
|
|
8608afffdc | ||
|
|
9a280b0140 | ||
|
|
7baf58ef6d | ||
|
|
2190921c85 | ||
|
|
9fa8de7baa | ||
|
|
ed9448a6ee | ||
|
|
15224a9ac5 | ||
|
|
6e00fd92ef | ||
|
|
6fdd1ce41a | ||
|
|
91d4cd6824 | ||
|
|
c7254a0c30 | ||
|
|
38f01a6b7d | ||
|
|
f194a7ea3e | ||
|
|
05a7ba98c1 | ||
|
|
edc513a3df | ||
|
|
39212a049c | ||
|
|
9b4f370834 | ||
|
|
aba85b036c | ||
|
|
6e86697996 | ||
|
|
cc90c912f5 | ||
|
|
efd20ef0d4 | ||
|
|
0c0aa1f3c3 | ||
|
|
231a475a17 | ||
|
|
94ea83c415 | ||
|
|
4b5b9baa78 | ||
|
|
3bf0d5b99f | ||
|
|
8ed81ac3e1 | ||
|
|
7992fe85d6 | ||
|
|
afe925a55e | ||
|
|
5e3f5f2b55 | ||
|
|
d4ad523eb3 | ||
|
|
e8c80d88a5 | ||
|
|
76241a7b2b | ||
|
|
1e4af9731d | ||
|
|
88327fb872 | ||
|
|
702499b97d | ||
|
|
da248414af | ||
|
|
af2c232c87 | ||
|
|
cca037b03c | ||
|
|
1d71bb5a79 | ||
|
|
ee4f2c735d | ||
|
|
4d559a63ec | ||
|
|
573e9b0d52 | ||
|
|
a2502109ab | ||
|
|
3cdece4945 | ||
|
|
520b825511 | ||
|
|
191401f2f1 | ||
|
|
8136d7fd54 | ||
|
|
5d1e486478 | ||
|
|
85b0b97ef2 | ||
|
|
471fab0591 | ||
|
|
6997ed83c4 | ||
|
|
a2ba36c16d | ||
|
|
109c79125d | ||
|
|
fbd49e0b79 | ||
|
|
1f20b6471c | ||
|
|
1d6a9f6e80 | ||
|
|
0a9f1a3cbf | ||
|
|
4f803832ad | ||
|
|
ef4aec7398 | ||
|
|
5bb3492616 | ||
|
|
78229baeab | ||
|
|
81f269e2a9 | ||
|
|
225b0f9377 | ||
|
|
30b90f9baa | ||
|
|
1293e473ca | ||
|
|
1a24a2d35e | ||
|
|
f0f1687c79 | ||
|
|
ded980bfc3 | ||
|
|
4cb56edebf | ||
|
|
c411151560 | ||
|
|
f52bd9f38a | ||
|
|
006d02cfaf | ||
|
|
263f96da87 | ||
|
|
f22affd836 | ||
|
|
f5667cefd4 | ||
|
|
7efce389b2 | ||
|
|
f59cff4f5d | ||
|
|
984f06ac40 | ||
|
|
9d4a12dfd4 | ||
|
|
94730567ab | ||
|
|
57db5e64de | ||
|
|
4d32968f2b | ||
|
|
10989e6927 | ||
|
|
62cc12be3c | ||
|
|
1874557b95 | ||
|
|
9a78547bf0 | ||
|
|
0b1bd9deb1 | ||
|
|
7202179d63 | ||
|
|
519a7df4cd | ||
|
|
3762728c84 | ||
|
|
bc3fa2b3fb | ||
|
|
57fca378bc | ||
|
|
eb718145c0 | ||
|
|
c87c1866ae | ||
|
|
b190423d96 | ||
|
|
edd3ab7cc9 | ||
|
|
4147f1d912 | ||
|
|
e4311da1a4 | ||
|
|
b7bb118c00 |
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
||||
24.11.1
|
||||
24.13.0
|
||||
|
||||
47
.github/workflows/build-mobile.yml
vendored
47
.github/workflows/build-mobile.yml
vendored
@@ -30,18 +30,6 @@ on:
|
||||
required: true
|
||||
IOS_CERTIFICATE_PASSWORD:
|
||||
required: true
|
||||
IOS_PROVISIONING_PROFILE:
|
||||
required: true
|
||||
IOS_PROVISIONING_PROFILE_SHARE_EXTENSION:
|
||||
required: true
|
||||
IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION:
|
||||
required: true
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE:
|
||||
required: true
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION:
|
||||
required: true
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION:
|
||||
required: true
|
||||
FASTLANE_TEAM_ID:
|
||||
required: true
|
||||
pull_request:
|
||||
@@ -96,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
|
||||
@@ -115,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
|
||||
@@ -165,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: |
|
||||
@@ -194,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
|
||||
@@ -240,35 +228,14 @@ jobs:
|
||||
mkdir -p ~/.appstoreconnect/private_keys
|
||||
echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
|
||||
|
||||
- name: Import Certificate and Provisioning Profiles
|
||||
- name: Import Certificate
|
||||
env:
|
||||
IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
|
||||
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
|
||||
IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
|
||||
IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_SHARE_EXTENSION }}
|
||||
IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION }}
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE }}
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION }}
|
||||
IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION }}
|
||||
ENVIRONMENT: ${{ inputs.environment || 'development' }}
|
||||
working-directory: ./mobile/ios
|
||||
run: |
|
||||
# Decode certificate
|
||||
echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12
|
||||
|
||||
# Decode provisioning profiles based on environment
|
||||
if [[ "$ENVIRONMENT" == "development" ]]; then
|
||||
echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE" | base64 --decode > profile_dev.mobileprovision
|
||||
echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_dev_share.mobileprovision
|
||||
echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_dev_widget.mobileprovision
|
||||
ls -lh profile_dev*.mobileprovision
|
||||
else
|
||||
echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision
|
||||
echo "$IOS_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_share.mobileprovision
|
||||
echo "$IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_widget.mobileprovision
|
||||
ls -lh profile*.mobileprovision
|
||||
fi
|
||||
|
||||
- name: Create keychain and import certificate
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
|
||||
@@ -319,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 }}
|
||||
|
||||
5
.github/workflows/docs-build.yml
vendored
5
.github/workflows/docs-build.yml
vendored
@@ -60,10 +60,11 @@ 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 }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
@@ -85,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 }}
|
||||
|
||||
41
.github/workflows/test.yml
vendored
41
.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 }}
|
||||
@@ -298,9 +298,9 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=immich-web install --frozen-lockfile
|
||||
run: pnpm --filter=immich-i18n install --frozen-lockfile
|
||||
- name: Format
|
||||
run: pnpm --filter=immich-web format:i18n
|
||||
run: pnpm --filter=immich-i18n format:fix
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
id: verify-changed-files
|
||||
@@ -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,17 +566,14 @@ 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 }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||
with:
|
||||
python-version: 3.11
|
||||
#cache: 'uv'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --extra cpu
|
||||
@@ -610,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 }}
|
||||
@@ -639,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 }}
|
||||
@@ -661,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 }}
|
||||
@@ -723,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 }}
|
||||
|
||||
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contributing to Immich
|
||||
|
||||
We appreciate every contribution, and we're happy about every new contributor. So please feel invited to help make Immich a better product!
|
||||
|
||||
## Getting started
|
||||
|
||||
To get you started quickly we have detailed guides for the dev setup on our [website](https://docs.immich.app/developer/setup). If you prefer, you can also use [Devcontainers](https://docs.immich.app/developer/devcontainers).
|
||||
There are also additional resources about Immich's architecture, database migrations, the use of OpenAPI, and more in our [developer documentation](https://docs.immich.app/developer/architecture).
|
||||
|
||||
## General
|
||||
|
||||
Please try to keep pull requests as focused as possible. A PR should do exactly one thing and not bleed into other, unrelated areas. The smaller a PR, the fewer changes are likely needed, and the quicker it will likely be merged. For larger/more impactful PRs, please reach out to us first to discuss your plans. The best way to do this is through our [Discord](https://discord.immich.app). We have a dedicated `#contributing` channel there. Additionally, please fill out the entire template when opening a PR.
|
||||
|
||||
## Finding work
|
||||
|
||||
If you are looking for something to work on, there are discussions and issues with a `good-first-issue` label on them. These are always a good starting point. If none of them sound interesting or fit your skill set, feel free to reach out on our Discord. We're happy to help you find something to work on!
|
||||
|
||||
## Use of generative AI
|
||||
|
||||
We generally discourage PRs entirely generated by an LLM. For any part generated by an LLM, please put extra effort into your self-review. By using generative AI without proper self-review, the time you save ends up being more work we need to put in for proper reviews and code cleanup. Please keep that in mind when submitting code by an LLM. Clearly state the use of LLMs/(generative) AI in your pull request as requested by the template.
|
||||
|
||||
## Feature freezes
|
||||
|
||||
From time to time, we put a feature freeze on parts of the codebase. For us, this means we won't accept most PRs that make changes in that area. Exempted from this are simple bug fixes that require only minor changes. We will close feature PRs that target a feature-frozen area, even if that feature is highly requested and you put a lot of work into it. Please keep that in mind, and if you're ever uncertain if a PR would be accepted, reach out to us first (e.g., in the aforementioned `#contributing` channel). We hate to throw away work. Currently, we have feature freezes on:
|
||||
|
||||
* Sharing/Asset ownership
|
||||
* (External) libraries
|
||||
|
||||
## Non-code contributions
|
||||
|
||||
If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team. All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated! If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated.
|
||||
@@ -1 +1 @@
|
||||
24.11.1
|
||||
24.13.0
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/node": "^24.10.4",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.0",
|
||||
"vite-tsconfig-paths": "^6.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-fetch-mock": "^0.4.0",
|
||||
"yaml": "^2.3.1"
|
||||
@@ -69,6 +69,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.1"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
@@ -146,6 +146,8 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
disable: false
|
||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||
# immich-prometheus:
|
||||
# container_name: immich_prometheus
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -77,13 +77,15 @@ services:
|
||||
- 5432:5432
|
||||
shm_size: 128mb
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||
immich-prometheus:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09
|
||||
image: prom/prometheus@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
@@ -95,7 +97,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.3.0-ubuntu@sha256:cee936306135e1925ab21dffa16f8a411535d16ab086bef2309339a8e74d62df
|
||||
image: grafana/grafana:12.3.1-ubuntu@sha256:d57f1365197aec34c4d80869d8ca45bb7787c7663904950dab214dfb40c1c2fd
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -69,6 +69,8 @@ services:
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.1
|
||||
24.13.0
|
||||
|
||||
@@ -4,6 +4,10 @@ sidebar_position: 2
|
||||
|
||||
# Setup
|
||||
|
||||
:::warning
|
||||
Make sure to read the [`CONTRIBUTING.md`](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before you dive into the code.
|
||||
:::
|
||||
|
||||
:::note
|
||||
If there's a feature you're planning to work on, just give us a heads up in [#contributing](https://discord.com/channels/979116623879368755/1071165397228855327) on [our Discord](https://discord.immich.app) so we can:
|
||||
|
||||
|
||||
@@ -71,6 +71,22 @@ For RKMPP to work:
|
||||
|
||||
5. (Optional) Enable hardware decoding for optimal performance.
|
||||
|
||||
<details>
|
||||
<summary>immich.json</summary>
|
||||
|
||||
If you use a [configuration file](/install/config-file.md), use the `accel` option to select the hardware (e.g. `qsv` for Intel or `nvenc` for Nvidia). Set `accelDecode` to `true` if you want hardware decoding.
|
||||
|
||||
```json
|
||||
{
|
||||
"ffmpeg": {
|
||||
"accel": "qsv",
|
||||
"accelDecode": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Single Compose File
|
||||
|
||||
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly.
|
||||
|
||||
@@ -95,11 +95,3 @@ Enter the cloud on the top right -> cog wheel on the top right -> select the syn
|
||||
If you delete/move photos in the local album on your device, it will not be reflected in the album on the server **even if** you click Sync albums
|
||||
It will only reflect files you add.
|
||||
:::
|
||||
|
||||
If the same asset is in more than one album it will only sync to the first album it's in, after that it won't sync again even if the user clicks sync albums manually.
|
||||
To overcome this limitation, the files must be removed from the ignore list by
|
||||
App settings -> Advanced -> Duplicate Assets -> Clear
|
||||
|
||||
:::info
|
||||
Cleaning duplicate assets from the list will cause all the previously uploaded duplicate files to be re-uploaded, the files will not actually be uploaded and will be rejected on the server side (due to duplication) but will be synchronized to the album and at the end will be added to the ignore list again at the end of the synchronization.
|
||||
:::
|
||||
|
||||
@@ -112,4 +112,40 @@ You can then make a new panel, specifying Prometheus as the data source for it.
|
||||
|
||||
-- TODO: add images and more details here
|
||||
|
||||
## Structured Logging
|
||||
|
||||
In addition to Prometheus metrics, Immich supports structured JSON logging which is ideal for log aggregation systems like Grafana Loki, ELK Stack, Datadog, Splunk, and others.
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, Immich outputs human-readable console logs. To enable JSON logging, set the `IMMICH_LOG_FORMAT` environment variable:
|
||||
|
||||
```bash
|
||||
IMMICH_LOG_FORMAT=json
|
||||
```
|
||||
|
||||
:::tip
|
||||
The default is `IMMICH_LOG_FORMAT=console` for human-readable logs with colors during development. For production deployments using log aggregation, use `IMMICH_LOG_FORMAT=json`.
|
||||
:::
|
||||
|
||||
### JSON Log Format
|
||||
|
||||
When enabled, logs are output in structured JSON format:
|
||||
|
||||
```json
|
||||
{"level":"log","pid":36,"timestamp":1766533331507,"message":"Initialized websocket server","context":"WebsocketRepository"}
|
||||
{"level":"warn","pid":48,"timestamp":1766533331629,"message":"Unable to open /build/www/index.html, skipping SSR.","context":"ApiService"}
|
||||
{"level":"error","pid":36,"timestamp":1766533331690,"message":"Failed to load plugin immich-core:","context":"Error"}
|
||||
```
|
||||
|
||||
This format includes:
|
||||
|
||||
- `level`: Log level (log, warn, error, etc.)
|
||||
- `pid`: Process ID
|
||||
- `timestamp`: Unix timestamp in milliseconds
|
||||
- `message`: Log message
|
||||
- `context`: Service or component that generated the log
|
||||
|
||||
For more information on log formats, see [`IMMICH_LOG_FORMAT`](/install/environment-variables.md#general).
|
||||
|
||||
[prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +34,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
|
||||
@@ -26,6 +26,12 @@ const config = {
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
// Mermaid diagrams
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
|
||||
plugins: [
|
||||
async function myPlugin(context, options) {
|
||||
return {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@docusaurus/core": "~3.9.0",
|
||||
"@docusaurus/preset-classic": "~3.9.0",
|
||||
"@docusaurus/theme-common": "~3.9.0",
|
||||
"@docusaurus/theme-mermaid": "~3.9.0",
|
||||
"@mdi/js": "^7.3.67",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
@@ -57,6 +58,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.1"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Overpass';
|
||||
src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations');
|
||||
font-weight: 1 999;
|
||||
font-family: 'GoogleSans';
|
||||
src: url('/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
|
||||
font-weight: 410 900;
|
||||
font-style: normal;
|
||||
ascent-override: 106.25%;
|
||||
size-adjust: 106.25%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Overpass Mono';
|
||||
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations');
|
||||
font-weight: 1 999;
|
||||
font-style: normal;
|
||||
font-family: 'GoogleSansCode';
|
||||
src: url('/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
|
||||
font-weight: 1 900;
|
||||
font-style: monospace;
|
||||
ascent-override: 106.25%;
|
||||
size-adjust: 106.25%;
|
||||
}
|
||||
@@ -37,7 +37,8 @@ img {
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
font-family: 'Overpass', sans-serif;
|
||||
font-family: 'GoogleSans', sans-serif;
|
||||
letter-spacing: 0.1px;
|
||||
--ifm-color-primary: #4250af;
|
||||
--ifm-color-primary-dark: #4250af;
|
||||
--ifm-color-primary-darker: #4250af;
|
||||
@@ -48,6 +49,16 @@ img {
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: 'GoogleSans', sans-serif;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #adcbfa;
|
||||
@@ -71,15 +82,22 @@ div[class^='announcementBar_'] {
|
||||
padding: 10px 10px 10px 16px;
|
||||
border-radius: 24px;
|
||||
margin-right: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu__list-item-collapsible {
|
||||
margin-right: 16px;
|
||||
border-radius: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu__link--active {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-of-contents__link {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
}
|
||||
|
||||
/* workaround for version switcher PR 15894 */
|
||||
@@ -88,13 +106,14 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-family: 'GoogleSansCode';
|
||||
}
|
||||
|
||||
.buy-button {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid transparent;
|
||||
font-family: 'Overpass', sans-serif;
|
||||
font-family: 'GoogleSans', sans-serif;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);
|
||||
|
||||
BIN
docs/static/fonts/GoogleSans/GoogleSans.ttf
vendored
Normal file
BIN
docs/static/fonts/GoogleSans/GoogleSans.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/GoogleSansCode/GoogleSansCode.ttf
vendored
Normal file
BIN
docs/static/fonts/GoogleSansCode/GoogleSansCode.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/overpass/Overpass-Italic.ttf
vendored
BIN
docs/static/fonts/overpass/Overpass-Italic.ttf
vendored
Binary file not shown.
BIN
docs/static/fonts/overpass/Overpass.ttf
vendored
BIN
docs/static/fonts/overpass/Overpass.ttf
vendored
Binary file not shown.
BIN
docs/static/fonts/overpass/OverpassMono.ttf
vendored
BIN
docs/static/fonts/overpass/OverpassMono.ttf
vendored
Binary file not shown.
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 +1 @@
|
||||
24.11.1
|
||||
24.13.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.3",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/node": "^24.10.4",
|
||||
"@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",
|
||||
@@ -54,6 +52,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.1"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ describe('/shared-links', () => {
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
let metadataAlbum: AlbumResponseDto;
|
||||
let deletedAlbum: AlbumResponseDto;
|
||||
let linkWithDeletedAlbum: SharedLinkResponseDto;
|
||||
let linkWithPassword: SharedLinkResponseDto;
|
||||
@@ -41,18 +40,9 @@ describe('/shared-links', () => {
|
||||
|
||||
[asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]);
|
||||
|
||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||
[album, deletedAlbum] = await Promise.all([
|
||||
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
|
||||
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
createAlbum(
|
||||
{
|
||||
createAlbumDto: {
|
||||
albumName: 'metadata album',
|
||||
assetIds: [asset1.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
),
|
||||
]);
|
||||
|
||||
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
|
||||
@@ -75,14 +65,14 @@ describe('/shared-links', () => {
|
||||
password: 'foo',
|
||||
}),
|
||||
utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
showMetadata: true,
|
||||
slug: 'metadata-album',
|
||||
slug: 'metadata-slug',
|
||||
}),
|
||||
utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
showMetadata: false,
|
||||
}),
|
||||
]);
|
||||
@@ -95,9 +85,7 @@ describe('/shared-links', () => {
|
||||
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(
|
||||
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
||||
);
|
||||
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`);
|
||||
});
|
||||
|
||||
it('should have correct asset count in meta tag for empty album', async () => {
|
||||
@@ -144,9 +132,7 @@ describe('/shared-links', () => {
|
||||
const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(
|
||||
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
||||
);
|
||||
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,12 +257,12 @@ describe('/shared-links', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return metadata for album shared link', async () => {
|
||||
it('should return metadata for individual shared link', async () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(0);
|
||||
expect(body.album).toBeDefined();
|
||||
expect(body.assets).toHaveLength(1);
|
||||
expect(body.album).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should not return metadata for album shared link without metadata', async () => {
|
||||
@@ -284,7 +270,7 @@ describe('/shared-links', () => {
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(1);
|
||||
expect(body.album).toBeDefined();
|
||||
expect(body.album).not.toBeDefined();
|
||||
|
||||
const asset = body.assets[0];
|
||||
expect(asset).not.toHaveProperty('exifInfo');
|
||||
|
||||
@@ -26,6 +26,5 @@ export const makeRandomImage = () => {
|
||||
if (!value) {
|
||||
throw new Error('Ran out of random asset data');
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
i18n/.prettierrc
Normal file
5
i18n/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"jsonRecursiveSort": true,
|
||||
"jsonSortOrder": "{\"/.*/\": \"lexical\"}",
|
||||
"plugins": ["prettier-plugin-sort-json"]
|
||||
}
|
||||
54
i18n/en.json
54
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?",
|
||||
@@ -734,6 +736,18 @@
|
||||
"checksum": "Checksum",
|
||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||
"city": "City",
|
||||
"cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?",
|
||||
"cleanup_confirm_prompt_title": "Remove from this device?",
|
||||
"cleanup_deleted_assets": "Moved {count} assets to device trash",
|
||||
"cleanup_deleting": "Moving to trash...",
|
||||
"cleanup_filter_description": "Choose which types of assets to remove in the cleanup",
|
||||
"cleanup_found_assets": "Found {count} backed up assets",
|
||||
"cleanup_icloud_shared_albums_excluded": "iCloud Shared Albums are excluded from the scan",
|
||||
"cleanup_no_assets_found": "No backed up assets found matching your criteria",
|
||||
"cleanup_preview_title": "Assets to remove ({count})",
|
||||
"cleanup_step3_description": "Scan for photos and videos that have been backed up to the server with the selected cutoff date and filter options",
|
||||
"cleanup_step4_summary": "{count} assets created before {date} are queued for removal from your device",
|
||||
"cleanup_trash_hint": "To fully reclaim storage space, open the system gallery app and empty the trash",
|
||||
"clear": "Clear",
|
||||
"clear_all": "Clear all",
|
||||
"clear_all_recent_searches": "Clear all recent searches",
|
||||
@@ -819,13 +833,20 @@
|
||||
"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",
|
||||
"current_server_address": "Current server address",
|
||||
"custom_date": "Custom date",
|
||||
"custom_locale": "Custom Locale",
|
||||
"custom_locale_description": "Format dates and numbers based on the language and the region",
|
||||
"custom_url": "Custom URL",
|
||||
"cutoff_date_description": "Remove photos and videos older than",
|
||||
"cutoff_day": "{count, plural, one {day} other {days}}",
|
||||
"cutoff_year": "{count, plural, one {year} other {years}}",
|
||||
"daily_title_text_date": "E, MMM dd",
|
||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||
"dark": "Dark",
|
||||
@@ -948,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",
|
||||
@@ -1082,6 +1107,7 @@
|
||||
"unable_to_scan_library": "Unable to scan library",
|
||||
"unable_to_set_feature_photo": "Unable to set feature photo",
|
||||
"unable_to_set_profile_picture": "Unable to set profile picture",
|
||||
"unable_to_set_rating": "Unable to set rating",
|
||||
"unable_to_submit_job": "Unable to submit job",
|
||||
"unable_to_trash_asset": "Unable to trash asset",
|
||||
"unable_to_unlink_account": "Unable to unlink account",
|
||||
@@ -1140,13 +1166,14 @@
|
||||
"features": "Features",
|
||||
"features_in_development": "Features in Development",
|
||||
"features_setting_description": "Manage the app features",
|
||||
"file_name": "File name",
|
||||
"file_name": "File name: {file_name}",
|
||||
"file_name_or_extension": "File name or extension",
|
||||
"file_size": "File size",
|
||||
"filename": "Filename",
|
||||
"filetype": "Filetype",
|
||||
"filter": "Filter",
|
||||
"filter_description": "Conditions to filter the target assets",
|
||||
"filter_options": "Filter options",
|
||||
"filter_people": "Filter people",
|
||||
"filter_places": "Filter places",
|
||||
"filters": "Filters",
|
||||
@@ -1159,6 +1186,9 @@
|
||||
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
|
||||
"forgot_pin_code_question": "Forgot your PIN?",
|
||||
"forward": "Forward",
|
||||
"free_up_space": "Free Up Space",
|
||||
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe",
|
||||
"free_up_space_settings_subtitle": "Free up device storage",
|
||||
"full_path": "Full path: {path}",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||
@@ -1275,6 +1305,8 @@
|
||||
"json_error": "JSON error",
|
||||
"keep": "Keep",
|
||||
"keep_all": "Keep All",
|
||||
"keep_favorites": "Keep favorites",
|
||||
"keep_favorites_description": "Favorite assets will not be deleted from your device",
|
||||
"keep_this_delete_others": "Keep this, delete others",
|
||||
"kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||
@@ -1434,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",
|
||||
@@ -1445,6 +1479,7 @@
|
||||
"move_down": "Move down",
|
||||
"move_off_locked_folder": "Move out of locked folder",
|
||||
"move_to": "Move to",
|
||||
"move_to_device_trash": "Move to device trash",
|
||||
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||
"move_to_locked_folder": "Move to locked folder",
|
||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
||||
@@ -1627,6 +1662,7 @@
|
||||
"photos_and_videos": "Photos & Videos",
|
||||
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
||||
"photos_from_previous_years": "Photos from previous years",
|
||||
"photos_only": "Photos only",
|
||||
"pick_a_location": "Pick a location",
|
||||
"pick_custom_range": "Custom range",
|
||||
"pick_date_range": "Select a date range",
|
||||
@@ -1702,10 +1738,12 @@
|
||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||
"query_asset_id": "Query Asset ID",
|
||||
"queue_status": "Queuing {count}/{total}",
|
||||
"rate_asset": "Rate Asset",
|
||||
"rating": "Star rating",
|
||||
"rating_clear": "Clear rating",
|
||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||
"rating_description": "Display the EXIF rating in the info panel",
|
||||
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
|
||||
"reaction_options": "Reaction options",
|
||||
"read_changelog": "Read Changelog",
|
||||
"readonly_mode_disabled": "Read-only mode disabled",
|
||||
@@ -1805,9 +1843,11 @@
|
||||
"saved_settings": "Saved settings",
|
||||
"say_something": "Say something",
|
||||
"scaffold_body_error_occurred": "Error occurred",
|
||||
"scan": "Scan",
|
||||
"scan_all_libraries": "Scan All Libraries",
|
||||
"scan_library": "Scan",
|
||||
"scan_settings": "Scan Settings",
|
||||
"scanning": "Scanning",
|
||||
"scanning_for_album": "Scanning for album...",
|
||||
"search": "Search",
|
||||
"search_albums": "Search albums",
|
||||
@@ -1879,6 +1919,7 @@
|
||||
"select_all_in": "Select all in {group}",
|
||||
"select_avatar_color": "Select avatar color",
|
||||
"select_count": "{count, plural, one {Select #} other {Select #}}",
|
||||
"select_cutoff_date": "Select cutoff date",
|
||||
"select_face": "Select face",
|
||||
"select_featured_photo": "Select featured photo",
|
||||
"select_from_computer": "Select from computer",
|
||||
@@ -2083,6 +2124,7 @@
|
||||
"sync": "Sync",
|
||||
"sync_albums": "Sync albums",
|
||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||
"sync_cloud_ids": "Sync Cloud IDs",
|
||||
"sync_local": "Sync Local",
|
||||
"sync_remote": "Sync Remote",
|
||||
"sync_status": "Sync Status",
|
||||
@@ -2153,7 +2195,7 @@
|
||||
"trigger": "Trigger",
|
||||
"trigger_asset_uploaded": "Asset Uploaded",
|
||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||
"trigger_description": "An event that kick off the workflow",
|
||||
"trigger_description": "An event that kicks off the workflow",
|
||||
"trigger_person_recognized": "Person Recognized",
|
||||
"trigger_person_recognized_description": "Triggered when a person is detected",
|
||||
"trigger_type": "Trigger type",
|
||||
@@ -2247,6 +2289,7 @@
|
||||
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
|
||||
"videos": "Videos",
|
||||
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
|
||||
"videos_only": "Videos only",
|
||||
"view": "View",
|
||||
"view_album": "View Album",
|
||||
"view_all": "View All",
|
||||
@@ -2296,6 +2339,7 @@
|
||||
"yes": "Yes",
|
||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||
"your_wifi_name": "Your Wi-Fi name",
|
||||
"zero_to_clear_rating": "press 0 to clear asset rating",
|
||||
"zoom_image": "Zoom Image",
|
||||
"zoom_to_bounds": "Zoom to bounds"
|
||||
}
|
||||
|
||||
13
i18n/package.json
Normal file
13
i18n/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sort-json": "^4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
experimental_monorepo_root = true
|
||||
|
||||
[tools]
|
||||
node = "24.11.1"
|
||||
node = "24.13.0"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.24.0"
|
||||
pnpm = "10.27.0"
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
java = "25.0.1"
|
||||
@@ -34,4 +34,4 @@ run = { task = ":i18n:format-fix" }
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
dir = "i18n"
|
||||
run = "pnpm dlx sort-json *.json"
|
||||
run = "pnpm run format:fix"
|
||||
|
||||
@@ -252,6 +252,40 @@ data class HashResult (
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class CloudIdResult (
|
||||
val assetId: String,
|
||||
val error: String? = null,
|
||||
val cloudId: String? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): CloudIdResult {
|
||||
val assetId = pigeonVar_list[0] as String
|
||||
val error = pigeonVar_list[1] as String?
|
||||
val cloudId = pigeonVar_list[2] as String?
|
||||
return CloudIdResult(assetId, error, cloudId)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
assetId,
|
||||
error,
|
||||
cloudId,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is CloudIdResult) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
@@ -275,6 +309,11 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
HashResult.fromList(it)
|
||||
}
|
||||
}
|
||||
133.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
CloudIdResult.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
@@ -296,6 +335,10 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
stream.write(132)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is CloudIdResult -> {
|
||||
stream.write(133)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
@@ -315,6 +358,7 @@ interface NativeSyncApi {
|
||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||
fun cancelHashing()
|
||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||
|
||||
companion object {
|
||||
/** The codec used by NativeSyncApi. */
|
||||
@@ -508,6 +552,23 @@ interface NativeSyncApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val assetIdsArg = args[0] as List<String>
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getCloudIdForAssetIds(assetIdsArg))
|
||||
} catch (exception: Throwable) {
|
||||
MessagesPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
@@ -21,7 +22,6 @@ import kotlinx.coroutines.sync.withPermit
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
sealed class AssetResult {
|
||||
data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult()
|
||||
@@ -298,7 +298,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
var bytesRead: Int
|
||||
val buffer = ByteArray(HASH_BUFFER_SIZE)
|
||||
while (inputStream.read(buffer).also { bytesRead = it } > 0) {
|
||||
coroutineContext.ensureActive()
|
||||
currentCoroutineContext().ensureActive()
|
||||
digest.update(buffer, 0, bytesRead)
|
||||
}
|
||||
} ?: return HashResult(assetId, "Cannot open input stream for asset", null)
|
||||
@@ -316,4 +316,10 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
||||
hashTask?.cancel()
|
||||
hashTask = null
|
||||
}
|
||||
|
||||
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
1
mobile/drift_schemas/main/drift_schema_v15.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v15.json
generated
Normal file
File diff suppressed because one or more lines are too long
1
mobile/drift_schemas/main/drift_schema_v16.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v16.json
generated
Normal file
File diff suppressed because one or more lines are too long
1
mobile/drift_schemas/main/drift_schema_v17.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v17.json
generated
Normal file
File diff suppressed because one or more lines are too long
BIN
mobile/fonts/GoogleSans/GoogleSans-Bold.ttf
Normal file
BIN
mobile/fonts/GoogleSans/GoogleSans-Bold.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSans/GoogleSans-Italic.ttf
Normal file
BIN
mobile/fonts/GoogleSans/GoogleSans-Italic.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSans/GoogleSans-Medium.ttf
Normal file
BIN
mobile/fonts/GoogleSans/GoogleSans-Medium.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSans/GoogleSans-Regular.ttf
Normal file
BIN
mobile/fonts/GoogleSans/GoogleSans-Regular.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSans/GoogleSans-SemiBold.ttf
Normal file
BIN
mobile/fonts/GoogleSans/GoogleSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-Medium.ttf
Normal file
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-Medium.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-Regular.ttf
Normal file
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-Regular.ttf
Normal file
Binary file not shown.
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf
Normal file
BIN
mobile/fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf
Normal file
Binary file not shown.
3
mobile/ios/.gitignore
vendored
3
mobile/ios/.gitignore
vendored
@@ -33,4 +33,5 @@ Runner/GeneratedPluginRegistrant.*
|
||||
!default.perspectivev3
|
||||
|
||||
fastlane/report.xml
|
||||
Gemfile.lock
|
||||
Gemfile.lock
|
||||
certs/
|
||||
@@ -312,6 +312,39 @@ struct HashResult: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct CloudIdResult: Hashable {
|
||||
var assetId: String
|
||||
var error: String? = nil
|
||||
var cloudId: String? = nil
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> CloudIdResult? {
|
||||
let assetId = pigeonVar_list[0] as! String
|
||||
let error: String? = nilOrValue(pigeonVar_list[1])
|
||||
let cloudId: String? = nilOrValue(pigeonVar_list[2])
|
||||
|
||||
return CloudIdResult(
|
||||
assetId: assetId,
|
||||
error: error,
|
||||
cloudId: cloudId
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
assetId,
|
||||
error,
|
||||
cloudId,
|
||||
]
|
||||
}
|
||||
static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
override func readValue(ofType type: UInt8) -> Any? {
|
||||
switch type {
|
||||
@@ -323,6 +356,8 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||
case 132:
|
||||
return HashResult.fromList(self.readValue() as! [Any?])
|
||||
case 133:
|
||||
return CloudIdResult.fromList(self.readValue() as! [Any?])
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
}
|
||||
@@ -343,6 +378,9 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||
} else if let value = value as? HashResult {
|
||||
super.writeByte(132)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? CloudIdResult {
|
||||
super.writeByte(133)
|
||||
super.writeValue(value.toList())
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
}
|
||||
@@ -377,6 +415,7 @@ protocol NativeSyncApi {
|
||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||
func cancelHashing() throws
|
||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
@@ -560,5 +599,22 @@ class NativeSyncApiSetup {
|
||||
} else {
|
||||
getTrashedAssetsChannel.setMessageHandler(nil)
|
||||
}
|
||||
let getCloudIdForAssetIdsChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||
if let api = api {
|
||||
getCloudIdForAssetIdsChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let assetIdsArg = args[0] as! [String]
|
||||
do {
|
||||
let result = try api.getCloudIdForAssetIds(assetIds: assetIdsArg)
|
||||
reply(wrapResult(result))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getCloudIdForAssetIdsChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,31 +19,31 @@ struct AssetWrapper: Hashable, Equatable {
|
||||
|
||||
class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
static let name = "NativeSyncApi"
|
||||
|
||||
|
||||
static func register(with registrar: any FlutterPluginRegistrar) {
|
||||
let instance = NativeSyncApiImpl()
|
||||
NativeSyncApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance)
|
||||
registrar.publish(instance)
|
||||
}
|
||||
|
||||
|
||||
func detachFromEngine(for registrar: any FlutterPluginRegistrar) {
|
||||
super.detachFromEngine()
|
||||
}
|
||||
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private let changeTokenKey = "immich:changeToken"
|
||||
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
||||
private let recoveredAlbumSubType = 1000000219
|
||||
|
||||
|
||||
private var hashTask: Task<Void?, Error>?
|
||||
private static let hashCancelledCode = "HASH_CANCELLED"
|
||||
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
||||
|
||||
|
||||
|
||||
|
||||
init(with defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 16, *)
|
||||
private func getChangeToken() -> PHPersistentChangeToken? {
|
||||
guard let data = defaults.data(forKey: changeTokenKey) else {
|
||||
@@ -51,7 +51,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
}
|
||||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 16, *)
|
||||
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
|
||||
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
|
||||
@@ -59,18 +59,18 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
}
|
||||
defaults.set(data, forKey: changeTokenKey)
|
||||
}
|
||||
|
||||
|
||||
func clearSyncCheckpoint() -> Void {
|
||||
defaults.removeObject(forKey: changeTokenKey)
|
||||
}
|
||||
|
||||
|
||||
func checkpointSync() {
|
||||
guard #available(iOS 16, *) else {
|
||||
return
|
||||
}
|
||||
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||
}
|
||||
|
||||
|
||||
func shouldFullSync() -> Bool {
|
||||
guard #available(iOS 16, *),
|
||||
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
||||
@@ -78,36 +78,36 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
|
||||
// Cannot fetch persistent changes
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func getAlbums() throws -> [PlatformAlbum] {
|
||||
var albums: [PlatformAlbum] = []
|
||||
|
||||
|
||||
albumTypes.forEach { type in
|
||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||
for i in 0..<collections.count {
|
||||
let album = collections.object(at: i)
|
||||
|
||||
|
||||
// Ignore recovered album
|
||||
if(album.assetCollectionSubtype.rawValue == self.recoveredAlbumSubType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
||||
options.includeHiddenAssets = false
|
||||
|
||||
|
||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||
|
||||
|
||||
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
||||
|
||||
|
||||
var domainAlbum = PlatformAlbum(
|
||||
id: album.localIdentifier,
|
||||
name: album.localizedTitle!,
|
||||
@@ -115,57 +115,57 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
isCloud: isCloud,
|
||||
assetCount: Int64(assets.count)
|
||||
)
|
||||
|
||||
|
||||
if let firstAsset = assets.firstObject {
|
||||
domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) }
|
||||
}
|
||||
|
||||
|
||||
albums.append(domainAlbum)
|
||||
}
|
||||
}
|
||||
return albums.sorted { $0.id < $1.id }
|
||||
}
|
||||
|
||||
|
||||
func getMediaChanges() throws -> SyncDelta {
|
||||
guard #available(iOS 16, *) else {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||
}
|
||||
|
||||
|
||||
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
|
||||
}
|
||||
|
||||
|
||||
guard let storedToken = getChangeToken() else {
|
||||
// No token exists, definitely need a full sync
|
||||
print("MediaManager::getMediaChanges: No token found")
|
||||
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
|
||||
}
|
||||
|
||||
|
||||
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
||||
if storedToken == currentToken {
|
||||
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||
|
||||
|
||||
var updatedAssets: Set<AssetWrapper> = []
|
||||
var deletedAssets: Set<String> = []
|
||||
|
||||
|
||||
for change in changes {
|
||||
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
||||
|
||||
|
||||
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||
|
||||
|
||||
if (updated.isEmpty) { continue }
|
||||
|
||||
|
||||
let options = PHFetchOptions()
|
||||
options.includeHiddenAssets = false
|
||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||
for i in 0..<result.count {
|
||||
let asset = result.object(at: i)
|
||||
|
||||
|
||||
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
||||
let predicate = PlatformAsset(
|
||||
id: asset.localIdentifier,
|
||||
@@ -178,25 +178,25 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
||||
updatedAssets.insert(domainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let updates = Array(updatedAssets.map { $0.asset })
|
||||
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||
guard !assets.isEmpty else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
|
||||
var albumAssets: [String: [String]] = [:]
|
||||
|
||||
|
||||
for type in albumTypes {
|
||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||
collections.enumerateObjects { (album, _, _) in
|
||||
@@ -211,13 +211,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
}
|
||||
return albumAssets
|
||||
}
|
||||
|
||||
|
||||
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||
guard let album = collections.firstObject else {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
var ids: [String] = []
|
||||
let options = PHFetchOptions()
|
||||
options.includeHiddenAssets = false
|
||||
@@ -227,13 +227,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
|
||||
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
|
||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||
guard let album = collections.firstObject else {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
let options = PHFetchOptions()
|
||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||
@@ -241,32 +241,32 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||
return Int64(assets.count)
|
||||
}
|
||||
|
||||
|
||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||
guard let album = collections.firstObject else {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
let options = PHFetchOptions()
|
||||
options.includeHiddenAssets = false
|
||||
if(updatedTimeCond != nil) {
|
||||
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||
}
|
||||
|
||||
|
||||
let result = getAssetsFromAlbum(in: album, options: options)
|
||||
if(result.count == 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
var assets: [PlatformAsset] = []
|
||||
result.enumerateObjects { (asset, _, _) in
|
||||
assets.append(asset.toPlatformAsset())
|
||||
}
|
||||
return assets
|
||||
}
|
||||
|
||||
|
||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) {
|
||||
if let prevTask = hashTask {
|
||||
prevTask.cancel()
|
||||
@@ -284,11 +284,11 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
missingAssetIds.remove(asset.localIdentifier)
|
||||
assets.append(asset)
|
||||
}
|
||||
|
||||
|
||||
if Task.isCancelled {
|
||||
return self?.completeWhenActive(for: completion, with: Self.hashCancelled)
|
||||
}
|
||||
|
||||
|
||||
await withTaskGroup(of: HashResult?.self) { taskGroup in
|
||||
var results = [HashResult]()
|
||||
results.reserveCapacity(assets.count)
|
||||
@@ -301,28 +301,28 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for await result in taskGroup {
|
||||
guard let result = result else {
|
||||
return self?.completeWhenActive(for: completion, with: Self.hashCancelled)
|
||||
}
|
||||
results.append(result)
|
||||
}
|
||||
|
||||
|
||||
for missing in missingAssetIds {
|
||||
results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil))
|
||||
}
|
||||
|
||||
|
||||
return self?.completeWhenActive(for: completion, with: .success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cancelHashing() {
|
||||
hashTask?.cancel()
|
||||
hashTask = nil
|
||||
}
|
||||
|
||||
|
||||
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
||||
class RequestRef {
|
||||
var id: PHAssetResourceDataRequestID?
|
||||
@@ -332,21 +332,21 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
if Task.isCancelled {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
guard let resource = asset.getResource() else {
|
||||
return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil)
|
||||
}
|
||||
|
||||
|
||||
if Task.isCancelled {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let options = PHAssetResourceRequestOptions()
|
||||
options.isNetworkAccessAllowed = allowNetworkAccess
|
||||
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
var hasher = Insecure.SHA1()
|
||||
|
||||
|
||||
requestRef.id = PHAssetResourceManager.default().requestData(
|
||||
for: resource,
|
||||
options: options,
|
||||
@@ -377,11 +377,11 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
PHAssetResourceManager.default().cancelDataRequest(requestId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||
}
|
||||
|
||||
|
||||
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||
// Ensure to actually getting all assets for the Recents album
|
||||
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
|
||||
@@ -390,4 +390,28 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
return PHAsset.fetchAssets(in: album, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] {
|
||||
guard #available(iOS 16, *) else {
|
||||
return assetIds.map { CloudIdResult(assetId: $0) }
|
||||
}
|
||||
|
||||
var mappings: [CloudIdResult] = []
|
||||
let result = PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: assetIds)
|
||||
for (key, value) in result {
|
||||
switch value {
|
||||
case .success(let cloudIdentifier):
|
||||
let cloudId = cloudIdentifier.stringValue
|
||||
// Ignores invalid cloud ids of the format "GUID:ID:". Valid Ids are of the form "GUID:ID:HASH"
|
||||
if !cloudId.hasSuffix(":") {
|
||||
mappings.append(CloudIdResult(assetId: key, cloudId: cloudId))
|
||||
} else {
|
||||
mappings.append(CloudIdResult(assetId: key, error: "Incomplete Cloud Id: \(cloudId)"))
|
||||
}
|
||||
case .failure(let error):
|
||||
mappings.append(CloudIdResult(assetId: key, error: "Error getting Cloud Id: \(error.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ def get_version_from_pubspec
|
||||
end
|
||||
|
||||
# Helper method to configure code signing for all targets
|
||||
def configure_code_signing(bundle_id_suffix: "")
|
||||
def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:)
|
||||
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
|
||||
|
||||
# Runner (main app)
|
||||
@@ -54,7 +54,7 @@ end
|
||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}",
|
||||
profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix} AppStore",
|
||||
profile_name: profile_name_main,
|
||||
targets: ["Runner"]
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ end
|
||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension",
|
||||
profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension AppStore",
|
||||
profile_name: profile_name_share,
|
||||
targets: ["ShareExtension"]
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ end
|
||||
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
|
||||
code_sign_identity: CODE_SIGN_IDENTITY,
|
||||
bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget",
|
||||
profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget AppStore",
|
||||
profile_name: profile_name_widget,
|
||||
targets: ["WidgetExtension"]
|
||||
)
|
||||
end
|
||||
@@ -87,7 +87,10 @@ end
|
||||
bundle_id_suffix: "",
|
||||
configuration: "Release",
|
||||
distribute_external: true,
|
||||
version_number: nil
|
||||
version_number: nil,
|
||||
profile_name_main:,
|
||||
profile_name_share:,
|
||||
profile_name_widget:
|
||||
)
|
||||
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
|
||||
app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}"
|
||||
@@ -115,9 +118,9 @@ end
|
||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
||||
export_options: {
|
||||
provisioningProfiles: {
|
||||
"#{app_identifier}" => "#{app_identifier} AppStore",
|
||||
"#{app_identifier}.ShareExtension" => "#{app_identifier}.ShareExtension AppStore",
|
||||
"#{app_identifier}.Widget" => "#{app_identifier}.Widget AppStore"
|
||||
"#{app_identifier}" => profile_name_main,
|
||||
"#{app_identifier}.ShareExtension" => profile_name_share,
|
||||
"#{app_identifier}.Widget" => profile_name_widget
|
||||
},
|
||||
signingStyle: "manual",
|
||||
signingCertificate: CODE_SIGN_IDENTITY
|
||||
@@ -136,20 +139,35 @@ end
|
||||
lane :gha_testflight_dev do
|
||||
api_key = get_api_key
|
||||
|
||||
# Install development provisioning profiles
|
||||
install_provisioning_profile(path: "profile_dev.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_dev_share.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_dev_widget.mobileprovision")
|
||||
# Download and install provisioning profiles from App Store Connect
|
||||
# Certificate is imported by GHA workflow into build.keychain
|
||||
# Capture profile names after each sigh call
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
|
||||
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
# Configure code signing for dev bundle IDs
|
||||
configure_code_signing(bundle_id_suffix: "development")
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
|
||||
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
|
||||
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
# Configure code signing for dev bundle IDs using the downloaded profile names
|
||||
configure_code_signing(
|
||||
bundle_id_suffix: "development",
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
)
|
||||
|
||||
# Build and upload
|
||||
build_and_upload(
|
||||
api_key: api_key,
|
||||
bundle_id_suffix: "development",
|
||||
configuration: "Profile",
|
||||
distribute_external: false
|
||||
distribute_external: false,
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
)
|
||||
end
|
||||
|
||||
@@ -157,20 +175,33 @@ end
|
||||
lane :gha_release_prod do
|
||||
api_key = get_api_key
|
||||
|
||||
# Install provisioning profiles
|
||||
install_provisioning_profile(path: "profile.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_share.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_widget.mobileprovision")
|
||||
# Download and install provisioning profiles from App Store Connect
|
||||
# Certificate is imported by GHA workflow into build.keychain
|
||||
sigh(api_key: api_key, app_identifier: BASE_BUNDLE_ID, force: true)
|
||||
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.ShareExtension", force: true)
|
||||
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.Widget", force: true)
|
||||
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
|
||||
# Configure code signing for production bundle IDs
|
||||
configure_code_signing
|
||||
configure_code_signing(
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
)
|
||||
|
||||
# Build and upload with version number
|
||||
build_and_upload(
|
||||
api_key: api_key,
|
||||
version_number: get_version_from_pubspec,
|
||||
distribute_external: false,
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
)
|
||||
end
|
||||
|
||||
@@ -215,13 +246,26 @@ end
|
||||
# Use the same build process as production, just skip the upload
|
||||
# This ensures PR builds validate the same way as production builds
|
||||
|
||||
# Install provisioning profiles (use development profiles for PR builds)
|
||||
install_provisioning_profile(path: "profile_dev.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_dev_share.mobileprovision")
|
||||
install_provisioning_profile(path: "profile_dev_widget.mobileprovision")
|
||||
api_key = get_api_key
|
||||
|
||||
# Download and install provisioning profiles from App Store Connect
|
||||
# Certificate is imported by GHA workflow into build.keychain
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
|
||||
main_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
|
||||
share_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
|
||||
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
|
||||
|
||||
# Configure code signing for dev bundle IDs
|
||||
configure_code_signing(bundle_id_suffix: "development")
|
||||
configure_code_signing(
|
||||
bundle_id_suffix: "development",
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
)
|
||||
|
||||
# Build the app (same as gha_testflight_dev but without upload)
|
||||
build_app(
|
||||
@@ -233,9 +277,9 @@ end
|
||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
||||
export_options: {
|
||||
provisioningProfiles: {
|
||||
"#{BASE_BUNDLE_ID}.development" => "#{BASE_BUNDLE_ID}.development AppStore",
|
||||
"#{BASE_BUNDLE_ID}.development.ShareExtension" => "#{BASE_BUNDLE_ID}.development.ShareExtension AppStore",
|
||||
"#{BASE_BUNDLE_ID}.development.Widget" => "#{BASE_BUNDLE_ID}.development.Widget AppStore"
|
||||
"#{BASE_BUNDLE_ID}.development" => main_profile_name,
|
||||
"#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name,
|
||||
"#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name
|
||||
},
|
||||
signingStyle: "manual",
|
||||
signingCertificate: CODE_SIGN_IDENTITY
|
||||
|
||||
@@ -4,6 +4,8 @@ const int noDbId = -9223372036854775808; // from Isar
|
||||
const double downloadCompleted = -1;
|
||||
const double downloadFailed = -2;
|
||||
|
||||
const String kMobileMetadataKey = "mobile-app";
|
||||
|
||||
// Number of log entries to retain on app start
|
||||
const int kLogTruncateLimit = 2000;
|
||||
|
||||
|
||||
@@ -7,3 +7,7 @@ enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||
enum SortUserBy { id }
|
||||
|
||||
enum ActionSource { timeline, viewer }
|
||||
|
||||
enum CleanupStep { selectDate, filterOptions, scan, delete }
|
||||
|
||||
enum AssetFilterType { all, photosOnly, videosOnly }
|
||||
|
||||
@@ -51,4 +51,4 @@ const Map<String, Locale> locales = {
|
||||
|
||||
const String translationsPath = 'assets/i18n';
|
||||
|
||||
const List<Locale> localesNotSupportedByOverpass = [Locale('el', 'GR'), Locale('sr', 'Cyrl')];
|
||||
const List<Locale> localesNotSupportedByAppFont = [Locale('el', 'GR'), Locale('sr', 'Cyrl')];
|
||||
|
||||
1
mobile/lib/domain/models/asset/asset_edit.model.dart
Normal file
1
mobile/lib/domain/models/asset/asset_edit.model.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum AssetEditAction { rotate, crop, mirror, other }
|
||||
62
mobile/lib/domain/models/asset/asset_metadata.model.dart
Normal file
62
mobile/lib/domain/models/asset/asset_metadata.model.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
enum RemoteAssetMetadataKey {
|
||||
mobileApp("mobile-app");
|
||||
|
||||
final String key;
|
||||
|
||||
const RemoteAssetMetadataKey(this.key);
|
||||
}
|
||||
|
||||
abstract class RemoteAssetMetadataValue {
|
||||
const RemoteAssetMetadataValue();
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
}
|
||||
|
||||
class RemoteAssetMetadataItem {
|
||||
final RemoteAssetMetadataKey key;
|
||||
final RemoteAssetMetadataValue value;
|
||||
|
||||
const RemoteAssetMetadataItem({required this.key, required this.value});
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return {'key': key.key, 'value': value};
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteAssetMobileAppMetadata extends RemoteAssetMetadataValue {
|
||||
final String? cloudId;
|
||||
final String? createdAt;
|
||||
final String? adjustmentTime;
|
||||
final String? latitude;
|
||||
final String? longitude;
|
||||
|
||||
const RemoteAssetMobileAppMetadata({
|
||||
this.cloudId,
|
||||
this.createdAt,
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, Object?>{};
|
||||
if (cloudId != null) {
|
||||
map["iCloudId"] = cloudId;
|
||||
}
|
||||
if (createdAt != null) {
|
||||
map["createdAt"] = createdAt;
|
||||
}
|
||||
if (adjustmentTime != null) {
|
||||
map["adjustmentTime"] = adjustmentTime;
|
||||
}
|
||||
if (latitude != null) {
|
||||
map["latitude"] = latitude;
|
||||
}
|
||||
if (longitude != null) {
|
||||
map["longitude"] = longitude;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ sealed class BaseAsset {
|
||||
final int? durationInSeconds;
|
||||
final bool isFavorite;
|
||||
final String? livePhotoVideoId;
|
||||
final bool isEdited;
|
||||
|
||||
const BaseAsset({
|
||||
required this.name,
|
||||
@@ -34,6 +35,7 @@ sealed class BaseAsset {
|
||||
this.durationInSeconds,
|
||||
this.isFavorite = false,
|
||||
this.livePhotoVideoId,
|
||||
required this.isEdited,
|
||||
});
|
||||
|
||||
bool get isImage => type == AssetType.image;
|
||||
|
||||
@@ -3,6 +3,7 @@ part of 'base_asset.model.dart';
|
||||
class LocalAsset extends BaseAsset {
|
||||
final String id;
|
||||
final String? remoteAssetId;
|
||||
final String? cloudId;
|
||||
final int orientation;
|
||||
|
||||
final DateTime? adjustmentTime;
|
||||
@@ -12,6 +13,7 @@ class LocalAsset extends BaseAsset {
|
||||
const LocalAsset({
|
||||
required this.id,
|
||||
String? remoteId,
|
||||
this.cloudId,
|
||||
required super.name,
|
||||
super.checksum,
|
||||
required super.type,
|
||||
@@ -26,6 +28,7 @@ class LocalAsset extends BaseAsset {
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required super.isEdited,
|
||||
}) : remoteAssetId = remoteId;
|
||||
|
||||
@override
|
||||
@@ -53,12 +56,14 @@ class LocalAsset extends BaseAsset {
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
remoteId: ${remoteId ?? "<NA>"}
|
||||
remoteId: ${remoteId ?? "<NA>"},
|
||||
cloudId: ${cloudId ?? "<NA>"},
|
||||
checksum: ${checksum ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
orientation: $orientation,
|
||||
adjustmentTime: $adjustmentTime,
|
||||
latitude: ${latitude ?? "<NA>"},
|
||||
longitude: ${longitude ?? "<NA>"},
|
||||
orientation: $orientation,
|
||||
adjustmentTime: $adjustmentTime,
|
||||
latitude: ${latitude ?? "<NA>"},
|
||||
longitude: ${longitude ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
@@ -69,6 +74,7 @@ class LocalAsset extends BaseAsset {
|
||||
if (identical(this, other)) return true;
|
||||
return super == other &&
|
||||
id == other.id &&
|
||||
cloudId == other.cloudId &&
|
||||
orientation == other.orientation &&
|
||||
adjustmentTime == other.adjustmentTime &&
|
||||
latitude == other.latitude &&
|
||||
@@ -88,6 +94,7 @@ class LocalAsset extends BaseAsset {
|
||||
LocalAsset copyWith({
|
||||
String? id,
|
||||
String? remoteId,
|
||||
String? cloudId,
|
||||
String? name,
|
||||
String? checksum,
|
||||
AssetType? type,
|
||||
@@ -101,10 +108,12 @@ class LocalAsset extends BaseAsset {
|
||||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return LocalAsset(
|
||||
id: id ?? this.id,
|
||||
remoteId: remoteId ?? this.remoteId,
|
||||
cloudId: cloudId ?? this.cloudId,
|
||||
name: name ?? this.name,
|
||||
checksum: checksum ?? this.checksum,
|
||||
type: type ?? this.type,
|
||||
@@ -118,6 +127,7 @@ class LocalAsset extends BaseAsset {
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset {
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
required super.isEdited,
|
||||
}) : localAssetId = localId;
|
||||
|
||||
@override
|
||||
@@ -61,6 +62,7 @@ class RemoteAsset extends BaseAsset {
|
||||
stackId: ${stackId ?? "<NA>"},
|
||||
checksum: $checksum,
|
||||
livePhotoVideoId: ${livePhotoVideoId ?? "<NA>"},
|
||||
isEdited: $isEdited,
|
||||
}''';
|
||||
}
|
||||
|
||||
@@ -104,6 +106,7 @@ class RemoteAsset extends BaseAsset {
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -122,6 +125,7 @@ class RemoteAsset extends BaseAsset {
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -40,6 +40,9 @@ class HashService {
|
||||
_log.info("Starting hashing of assets");
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
// Migrate hashes from cloud ID to local ID so we don't have to re-hash them
|
||||
await _migrateHashes();
|
||||
|
||||
// Sorted by backupSelection followed by isCloud
|
||||
final localAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
|
||||
@@ -75,6 +78,15 @@ class HashService {
|
||||
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
Future<void> _migrateHashes() async {
|
||||
final hashMappings = await _localAssetRepository.getHashMappingFromCloudId();
|
||||
if (hashMappings.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _localAssetRepository.updateHashes(hashMappings);
|
||||
}
|
||||
|
||||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
@@ -18,6 +19,7 @@ import 'package:logging/logging.dart';
|
||||
|
||||
class LocalSyncService {
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
@@ -26,11 +28,13 @@ class LocalSyncService {
|
||||
|
||||
LocalSyncService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required LocalFilesManagerRepository localFilesManager,
|
||||
required StorageRepository storageRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_localFilesManager = localFilesManager,
|
||||
_storageRepository = storageRepository,
|
||||
@@ -47,6 +51,12 @@ class LocalSyncService {
|
||||
_log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentPlatform.isIOS) {
|
||||
final assets = await _localAssetRepository.getEmptyCloudIdAssets();
|
||||
await _mapIosCloudIds(assets);
|
||||
}
|
||||
|
||||
if (full || await _nativeSyncApi.shouldFullSync()) {
|
||||
_log.fine("Full sync request from ${full ? "user" : "native"}");
|
||||
return await fullSync();
|
||||
@@ -63,8 +73,9 @@ class LocalSyncService {
|
||||
|
||||
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
|
||||
final newAssets = delta.updates.toLocalAssets();
|
||||
await _localAlbumRepository.processDelta(
|
||||
updates: delta.updates.toLocalAssets(),
|
||||
updates: newAssets,
|
||||
deletes: delta.deletes,
|
||||
assetAlbums: delta.assetAlbums,
|
||||
);
|
||||
@@ -92,6 +103,8 @@ class LocalSyncService {
|
||||
}
|
||||
await updateAlbum(dbAlbum, album);
|
||||
}
|
||||
|
||||
await _mapIosCloudIds(newAssets);
|
||||
}
|
||||
await _nativeSyncApi.checkpointSync();
|
||||
} catch (e, s) {
|
||||
@@ -130,9 +143,12 @@ class LocalSyncService {
|
||||
try {
|
||||
_log.fine("Adding device album ${album.name}");
|
||||
|
||||
final assets = album.assetCount > 0 ? await _nativeSyncApi.getAssetsForAlbum(album.id) : <PlatformAsset>[];
|
||||
final assets = album.assetCount > 0
|
||||
? await _nativeSyncApi.getAssetsForAlbum(album.id).then((a) => a.toLocalAssets())
|
||||
: <LocalAsset>[];
|
||||
|
||||
await _localAlbumRepository.upsert(album, toUpsert: assets.toLocalAssets());
|
||||
await _localAlbumRepository.upsert(album, toUpsert: assets);
|
||||
await _mapIosCloudIds(assets);
|
||||
_log.fine("Successfully added device album ${album.name}");
|
||||
} catch (e, s) {
|
||||
_log.warning("Error while adding device album", e, s);
|
||||
@@ -202,13 +218,16 @@ class LocalSyncService {
|
||||
return false;
|
||||
}
|
||||
|
||||
final newAssets = await _nativeSyncApi.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime);
|
||||
final newAssets = await _nativeSyncApi
|
||||
.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime)
|
||||
.then((a) => a.toLocalAssets());
|
||||
|
||||
await _localAlbumRepository.upsert(
|
||||
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||
toUpsert: newAssets.toLocalAssets(),
|
||||
toUpsert: newAssets,
|
||||
);
|
||||
|
||||
await _mapIosCloudIds(newAssets);
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
|
||||
@@ -240,6 +259,7 @@ class LocalSyncService {
|
||||
if (dbAlbum.assetCount == 0) {
|
||||
_log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB.");
|
||||
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice);
|
||||
await _mapIosCloudIds(assetsInDevice);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -277,6 +297,7 @@ class LocalSyncService {
|
||||
}
|
||||
|
||||
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete);
|
||||
await _mapIosCloudIds(assetsToUpsert);
|
||||
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
@@ -285,6 +306,29 @@ class LocalSyncService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _mapIosCloudIds(List<LocalAsset> assets) async {
|
||||
if (!CurrentPlatform.isIOS || assets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final assetIds = assets.map((a) => a.id).toList();
|
||||
final cloudMapping = <String, String>{};
|
||||
final cloudIds = await _nativeSyncApi.getCloudIdForAssetIds(assetIds);
|
||||
for (int i = 0; i < cloudIds.length; i++) {
|
||||
final cloudIdResult = cloudIds[i];
|
||||
if (cloudIdResult.cloudId != null) {
|
||||
cloudMapping[cloudIdResult.assetId] = cloudIdResult.cloudId!;
|
||||
} else {
|
||||
final asset = assets.firstWhereOrNull((a) => a.id == cloudIdResult.assetId);
|
||||
_log.fine(
|
||||
"Cannot fetch cloudId for asset with id: ${cloudIdResult.assetId}, name: ${asset?.name}, createdAt: ${asset?.createdAt}. Error: ${cloudIdResult.error ?? "unknown"}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _localAlbumRepository.updateCloudMapping(cloudMapping);
|
||||
}
|
||||
|
||||
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||
@@ -360,6 +404,7 @@ extension on Iterable<PlatformAlbum> {
|
||||
name: e.name,
|
||||
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
assetCount: e.assetCount,
|
||||
isIosSharedAlbum: e.isCloud,
|
||||
),
|
||||
).toList();
|
||||
}
|
||||
@@ -391,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset {
|
||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class SearchService {
|
||||
}
|
||||
|
||||
return SearchResult(
|
||||
assets: response.assets.items.map((e) => e.toDto()).toList(),
|
||||
assets: response.assets.items.map((e) => e.toDto(false)).toList(),
|
||||
nextPage: response.assets.nextPage?.toInt(),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
@@ -54,7 +54,7 @@ class SearchService {
|
||||
}
|
||||
|
||||
extension on AssetResponseDto {
|
||||
RemoteAsset toDto() {
|
||||
RemoteAsset toDto(bool isEdited) {
|
||||
return RemoteAsset(
|
||||
id: id,
|
||||
name: originalFileName,
|
||||
@@ -77,6 +77,8 @@ extension on AssetResponseDto {
|
||||
thumbHash: thumbhash,
|
||||
localId: null,
|
||||
type: type.toAssetType(),
|
||||
// its a remote asset so it will always show the edited version
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +116,16 @@ class SyncStreamService {
|
||||
return;
|
||||
case SyncEntityType.assetDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetsV1(data.cast());
|
||||
case SyncEntityType.assetEditV1:
|
||||
return _syncStreamRepository.updateAssetEditsV1(data.cast());
|
||||
case SyncEntityType.assetEditDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetEditsV1(data.cast());
|
||||
case SyncEntityType.assetExifV1:
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||
case SyncEntityType.assetMetadataV1:
|
||||
return _syncStreamRepository.updateAssetsMetadataV1(data.cast());
|
||||
case SyncEntityType.assetMetadataDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
|
||||
case SyncEntityType.partnerAssetV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
|
||||
case SyncEntityType.partnerAssetBackfillV1:
|
||||
|
||||
@@ -79,6 +79,9 @@ class TimelineFactory {
|
||||
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||
TimelineService(_timelineRepository.fromAssets(assets, type));
|
||||
|
||||
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
||||
|
||||
TimelineService map(String userId, LatLngBounds bounds) =>
|
||||
TimelineService(_timelineRepository.map(userId, bounds, groupBy));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart' as m;
|
||||
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||
import 'package:immich_mobile/utils/isolate.dart';
|
||||
@@ -22,8 +23,13 @@ class BackgroundSyncManager {
|
||||
final SyncCallback? onHashingComplete;
|
||||
final SyncErrorCallback? onHashingError;
|
||||
|
||||
final SyncCallback? onCloudIdSyncStart;
|
||||
final SyncCallback? onCloudIdSyncComplete;
|
||||
final SyncErrorCallback? onCloudIdSyncError;
|
||||
|
||||
Cancelable<bool?>? _syncTask;
|
||||
Cancelable<void>? _syncWebsocketTask;
|
||||
Cancelable<void>? _cloudIdSyncTask;
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _linkedAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
@@ -38,6 +44,9 @@ class BackgroundSyncManager {
|
||||
this.onHashingStart,
|
||||
this.onHashingComplete,
|
||||
this.onHashingError,
|
||||
this.onCloudIdSyncStart,
|
||||
this.onCloudIdSyncComplete,
|
||||
this.onCloudIdSyncError,
|
||||
});
|
||||
|
||||
Future<void> cancel() async {
|
||||
@@ -55,6 +64,12 @@ class BackgroundSyncManager {
|
||||
_syncWebsocketTask?.cancel();
|
||||
_syncWebsocketTask = null;
|
||||
|
||||
if (_cloudIdSyncTask != null) {
|
||||
futures.add(_cloudIdSyncTask!.future);
|
||||
}
|
||||
_cloudIdSyncTask?.cancel();
|
||||
_cloudIdSyncTask = null;
|
||||
|
||||
if (_linkedAlbumSyncTask != null) {
|
||||
futures.add(_linkedAlbumSyncTask!.future);
|
||||
}
|
||||
@@ -121,7 +136,6 @@ class BackgroundSyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
Future<void> hashAssets() {
|
||||
if (_hashTask != null) {
|
||||
return _hashTask!.future;
|
||||
@@ -192,6 +206,25 @@ class BackgroundSyncManager {
|
||||
_linkedAlbumSyncTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncCloudIds() {
|
||||
if (_cloudIdSyncTask != null) {
|
||||
return _cloudIdSyncTask!.future;
|
||||
}
|
||||
|
||||
onCloudIdSyncStart?.call();
|
||||
|
||||
_cloudIdSyncTask = runInIsolateGentle(computation: m.syncCloudIds);
|
||||
return _cloudIdSyncTask!
|
||||
.whenComplete(() {
|
||||
onCloudIdSyncComplete?.call();
|
||||
_cloudIdSyncTask = null;
|
||||
})
|
||||
.catchError((error) {
|
||||
onCloudIdSyncError?.call(error.toString());
|
||||
_cloudIdSyncTask = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||
|
||||
175
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
175
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
// ignore: import_rule_openapi
|
||||
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||
|
||||
Future<void> syncCloudIds(ProviderContainer ref) async {
|
||||
if (!CurrentPlatform.isIOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
final db = ref.read(driftProvider);
|
||||
// Populate cloud IDs for local assets that don't have one yet
|
||||
await _populateCloudIds(db);
|
||||
|
||||
final serverInfo = await ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||
final canUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 4);
|
||||
if (!canUpdateMetadata) {
|
||||
Logger(
|
||||
'migrateCloudIds',
|
||||
).fine('Server version does not support asset metadata updates. Skipping cloudId migration.');
|
||||
return;
|
||||
}
|
||||
final canBulkUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 5);
|
||||
|
||||
// Wait for remote sync to complete, so we have up-to-date asset metadata entries
|
||||
try {
|
||||
await ref.read(syncStreamServiceProvider).sync();
|
||||
} catch (e, s) {
|
||||
Logger('migrateCloudIds').fine('Failed to complete remote sync before cloudId migration.', e, s);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the mapping for backed up assets that have a cloud ID locally but do not have a cloud ID on the server
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
Logger('migrateCloudIds').warning('Current user is null. Aborting cloudId migration.');
|
||||
return;
|
||||
}
|
||||
|
||||
final mappingsToUpdate = await _fetchCloudIdMappings(db, currentUser.id);
|
||||
final assetApi = ref.read(apiServiceProvider).assetsApi;
|
||||
|
||||
if (canBulkUpdateMetadata) {
|
||||
await _bulkUpdateCloudIds(assetApi, mappingsToUpdate);
|
||||
return;
|
||||
}
|
||||
await _sequentialUpdateCloudIds(assetApi, mappingsToUpdate);
|
||||
}
|
||||
|
||||
Future<void> _sequentialUpdateCloudIds(AssetsApi assetsApi, List<_CloudIdMapping> mappings) async {
|
||||
for (final mapping in mappings) {
|
||||
final item = AssetMetadataUpsertItemDto(
|
||||
key: kMobileMetadataKey,
|
||||
value: RemoteAssetMobileAppMetadata(
|
||||
cloudId: mapping.localAsset.cloudId,
|
||||
createdAt: mapping.localAsset.createdAt.toIso8601String(),
|
||||
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
|
||||
latitude: mapping.localAsset.latitude?.toString(),
|
||||
longitude: mapping.localAsset.longitude?.toString(),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await assetsApi.updateAssetMetadata(mapping.remoteAssetId, AssetMetadataUpsertDto(items: [item]));
|
||||
} catch (error, stack) {
|
||||
Logger('migrateCloudIds').warning('Failed to update metadata for asset ${mapping.remoteAssetId}', error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _bulkUpdateCloudIds(AssetsApi assetsApi, List<_CloudIdMapping> mappings) async {
|
||||
const batchSize = 10000;
|
||||
for (int i = 0; i < mappings.length; i += batchSize) {
|
||||
final endIndex = (i + batchSize > mappings.length) ? mappings.length : i + batchSize;
|
||||
final batch = mappings.sublist(i, endIndex);
|
||||
final items = <AssetMetadataBulkUpsertItemDto>[];
|
||||
for (final mapping in batch) {
|
||||
items.add(
|
||||
AssetMetadataBulkUpsertItemDto(
|
||||
assetId: mapping.remoteAssetId,
|
||||
key: kMobileMetadataKey,
|
||||
value: RemoteAssetMobileAppMetadata(
|
||||
cloudId: mapping.localAsset.cloudId,
|
||||
createdAt: mapping.localAsset.createdAt.toIso8601String(),
|
||||
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
|
||||
latitude: mapping.localAsset.latitude?.toString(),
|
||||
longitude: mapping.localAsset.longitude?.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await assetsApi.updateBulkAssetMetadata(AssetMetadataBulkUpsertDto(items: items));
|
||||
} catch (error, stack) {
|
||||
Logger('migrateCloudIds').warning('Failed to bulk update metadata', error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _populateCloudIds(Drift drift) async {
|
||||
final query = drift.localAssetEntity.selectOnly()
|
||||
..addColumns([drift.localAssetEntity.id])
|
||||
..where(drift.localAssetEntity.iCloudId.isNull());
|
||||
final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get();
|
||||
final cloudMapping = <String, String>{};
|
||||
final cloudIds = await NativeSyncApi().getCloudIdForAssetIds(ids);
|
||||
for (int i = 0; i < cloudIds.length; i++) {
|
||||
final cloudIdResult = cloudIds[i];
|
||||
if (cloudIdResult.cloudId != null) {
|
||||
cloudMapping[cloudIdResult.assetId] = cloudIdResult.cloudId!;
|
||||
} else {
|
||||
Logger('migrateCloudIds').fine(
|
||||
"Cannot fetch cloudId for asset with id: ${cloudIdResult.assetId}. Error: ${cloudIdResult.error ?? "unknown"}",
|
||||
);
|
||||
}
|
||||
}
|
||||
await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping);
|
||||
}
|
||||
|
||||
typedef _CloudIdMapping = ({String remoteAssetId, LocalAsset localAsset});
|
||||
|
||||
Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId) async {
|
||||
final isEdited = drift.assetEditEntity.assetId.isNotNull();
|
||||
final query =
|
||||
drift.remoteAssetEntity.select().join([
|
||||
leftOuterJoin(
|
||||
drift.localAssetEntity,
|
||||
drift.localAssetEntity.checksum.equalsExp(drift.remoteAssetEntity.checksum),
|
||||
),
|
||||
leftOuterJoin(
|
||||
drift.remoteAssetCloudIdEntity,
|
||||
drift.remoteAssetEntity.id.equalsExp(drift.remoteAssetCloudIdEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
drift.assetEditEntity,
|
||||
drift.assetEditEntity.assetId.equalsExp(drift.remoteAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..addColumns([isEdited])
|
||||
..where(
|
||||
// Only select assets that have a local cloud ID but either no remote cloud ID or a mismatched eTag
|
||||
drift.localAssetEntity.id.isNotNull() &
|
||||
drift.localAssetEntity.iCloudId.isNotNull() &
|
||||
drift.remoteAssetEntity.ownerId.equals(userId) &
|
||||
// Skip locked assets as we cannot update them without unlocking first
|
||||
drift.remoteAssetEntity.visibility.isNotValue(AssetVisibility.locked.index) &
|
||||
(drift.remoteAssetCloudIdEntity.cloudId.isNull() |
|
||||
((drift.remoteAssetCloudIdEntity.adjustmentTime.isNotExp(drift.localAssetEntity.adjustmentTime)) &
|
||||
(drift.remoteAssetCloudIdEntity.latitude.isNotExp(drift.localAssetEntity.latitude)) &
|
||||
(drift.remoteAssetCloudIdEntity.longitude.isNotExp(drift.localAssetEntity.longitude)) &
|
||||
(drift.remoteAssetCloudIdEntity.createdAt.isNotExp(drift.localAssetEntity.createdAt)))),
|
||||
);
|
||||
|
||||
return query.map((row) {
|
||||
return (
|
||||
remoteAssetId: row.read(drift.remoteAssetEntity.id)!,
|
||||
localAsset: row.readTable(drift.localAssetEntity).toDto(isEdited: row.read(isEdited)!),
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
23
mobile/lib/infrastructure/entities/asset_edit.entity.dart
Normal file
23
mobile/lib/infrastructure/entities/asset_edit.entity.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class AssetEditEntity extends Table with DriftDefaultsMixin {
|
||||
const AssetEditEntity();
|
||||
|
||||
TextColumn get id => text()();
|
||||
|
||||
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get action => intEnum<AssetEditAction>()();
|
||||
|
||||
BlobColumn get parameters => blob().map(editParameterConverter)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb(
|
||||
fromJson: (json) => json as Map<String, Object?>,
|
||||
);
|
||||
678
mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart
generated
Normal file
678
mobile/lib/infrastructure/entities/asset_edit.entity.drift.dart
generated
Normal file
@@ -0,0 +1,678 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/domain/models/asset/asset_edit.model.dart' as i2;
|
||||
import 'dart:typed_data' as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:drift/internal/modular.dart' as i6;
|
||||
|
||||
typedef $$AssetEditEntityTableCreateCompanionBuilder =
|
||||
i1.AssetEditEntityCompanion Function({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
});
|
||||
typedef $$AssetEditEntityTableUpdateCompanionBuilder =
|
||||
i1.AssetEditEntityCompanion Function({
|
||||
i0.Value<String> id,
|
||||
i0.Value<String> assetId,
|
||||
i0.Value<i2.AssetEditAction> action,
|
||||
i0.Value<Map<String, Object?>> parameters,
|
||||
});
|
||||
|
||||
final class $$AssetEditEntityTableReferences
|
||||
extends
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData
|
||||
> {
|
||||
$$AssetEditEntityTableReferences(
|
||||
super.$_db,
|
||||
super.$_table,
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i5.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$AssetEditEntityTable>('asset_edit_entity')
|
||||
.assetId,
|
||||
i6.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||
final $_column = $_itemColumn<String>('asset_id')!;
|
||||
|
||||
final manager = i5
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<i2.AssetEditAction, i2.AssetEditAction, int>
|
||||
get action => $composableBuilder(
|
||||
column: $table.action,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<
|
||||
Map<String, Object?>,
|
||||
Map<String, Object>,
|
||||
i3.Uint8List
|
||||
>
|
||||
get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get action => $composableBuilder(
|
||||
column: $table.action,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<i3.Uint8List> get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
|
||||
$$AssetEditEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int> get action =>
|
||||
$composableBuilder(column: $table.action, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
|
||||
get parameters => $composableBuilder(
|
||||
column: $table.parameters,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i5.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$AssetEditEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData,
|
||||
i1.$$AssetEditEntityTableFilterComposer,
|
||||
i1.$$AssetEditEntityTableOrderingComposer,
|
||||
i1.$$AssetEditEntityTableAnnotationComposer,
|
||||
$$AssetEditEntityTableCreateCompanionBuilder,
|
||||
$$AssetEditEntityTableUpdateCompanionBuilder,
|
||||
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
|
||||
i1.AssetEditEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
> {
|
||||
$$AssetEditEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$AssetEditEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$AssetEditEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$AssetEditEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () => i1
|
||||
.$$AssetEditEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<String> assetId = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetEditAction> action = const i0.Value.absent(),
|
||||
i0.Value<Map<String, Object?>> parameters =
|
||||
const i0.Value.absent(),
|
||||
}) => i1.AssetEditEntityCompanion(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
action: action,
|
||||
parameters: parameters,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
}) => i1.AssetEditEntityCompanion.insert(
|
||||
id: id,
|
||||
assetId: assetId,
|
||||
action: action,
|
||||
parameters: parameters,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
(e) => (
|
||||
e.readTable(table),
|
||||
i1.$$AssetEditEntityTableReferences(db, table, e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({assetId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins:
|
||||
<
|
||||
T extends i0.TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (assetId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.assetId,
|
||||
referencedTable: i1
|
||||
.$$AssetEditEntityTableReferences
|
||||
._assetIdTable(db),
|
||||
referencedColumn: i1
|
||||
.$$AssetEditEntityTableReferences
|
||||
._assetIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$AssetEditEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$AssetEditEntityTable,
|
||||
i1.AssetEditEntityData,
|
||||
i1.$$AssetEditEntityTableFilterComposer,
|
||||
i1.$$AssetEditEntityTableOrderingComposer,
|
||||
i1.$$AssetEditEntityTableAnnotationComposer,
|
||||
$$AssetEditEntityTableCreateCompanionBuilder,
|
||||
$$AssetEditEntityTableUpdateCompanionBuilder,
|
||||
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
|
||||
i1.AssetEditEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
>;
|
||||
|
||||
class $AssetEditEntityTable extends i4.AssetEditEntity
|
||||
with i0.TableInfo<$AssetEditEntityTable, i1.AssetEditEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$AssetEditEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
|
||||
'assetId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
|
||||
'asset_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
|
||||
),
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int>
|
||||
action =
|
||||
i0.GeneratedColumn<int>(
|
||||
'action',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<i2.AssetEditAction>(
|
||||
i1.$AssetEditEntityTable.$converteraction,
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<
|
||||
Map<String, Object?>,
|
||||
i3.Uint8List
|
||||
>
|
||||
parameters =
|
||||
i0.GeneratedColumn<i3.Uint8List>(
|
||||
'parameters',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.blob,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<Map<String, Object?>>(
|
||||
i1.$AssetEditEntityTable.$converterparameters,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, assetId, action, parameters];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'asset_edit_entity';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.AssetEditEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('asset_id')) {
|
||||
context.handle(
|
||||
_assetIdMeta,
|
||||
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_assetIdMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.AssetEditEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.AssetEditEntityData(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
assetId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}asset_id'],
|
||||
)!,
|
||||
action: i1.$AssetEditEntityTable.$converteraction.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}action'],
|
||||
)!,
|
||||
),
|
||||
parameters: i1.$AssetEditEntityTable.$converterparameters.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.blob,
|
||||
data['${effectivePrefix}parameters'],
|
||||
)!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$AssetEditEntityTable createAlias(String alias) {
|
||||
return $AssetEditEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AssetEditAction, int, int> $converteraction =
|
||||
const i0.EnumIndexConverter<i2.AssetEditAction>(
|
||||
i2.AssetEditAction.values,
|
||||
);
|
||||
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
|
||||
$converterparameters = i4.editParameterConverter;
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class AssetEditEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.AssetEditEntityData> {
|
||||
final String id;
|
||||
final String assetId;
|
||||
final i2.AssetEditAction action;
|
||||
final Map<String, Object?> parameters;
|
||||
const AssetEditEntityData({
|
||||
required this.id,
|
||||
required this.assetId,
|
||||
required this.action,
|
||||
required this.parameters,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['asset_id'] = i0.Variable<String>(assetId);
|
||||
{
|
||||
map['action'] = i0.Variable<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toSql(action),
|
||||
);
|
||||
}
|
||||
{
|
||||
map['parameters'] = i0.Variable<i3.Uint8List>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory AssetEditEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return AssetEditEntityData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
assetId: serializer.fromJson<String>(json['assetId']),
|
||||
action: i1.$AssetEditEntityTable.$converteraction.fromJson(
|
||||
serializer.fromJson<int>(json['action']),
|
||||
),
|
||||
parameters: i1.$AssetEditEntityTable.$converterparameters.fromJson(
|
||||
serializer.fromJson<Object?>(json['parameters']),
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'assetId': serializer.toJson<String>(assetId),
|
||||
'action': serializer.toJson<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toJson(action),
|
||||
),
|
||||
'parameters': serializer.toJson<Object?>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toJson(parameters),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
i1.AssetEditEntityData copyWith({
|
||||
String? id,
|
||||
String? assetId,
|
||||
i2.AssetEditAction? action,
|
||||
Map<String, Object?>? parameters,
|
||||
}) => i1.AssetEditEntityData(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
action: action ?? this.action,
|
||||
parameters: parameters ?? this.parameters,
|
||||
);
|
||||
AssetEditEntityData copyWithCompanion(i1.AssetEditEntityCompanion data) {
|
||||
return AssetEditEntityData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||
action: data.action.present ? data.action.value : this.action,
|
||||
parameters: data.parameters.present
|
||||
? data.parameters.value
|
||||
: this.parameters,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetEditEntityData(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('action: $action, ')
|
||||
..write('parameters: $parameters')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, assetId, action, parameters);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.AssetEditEntityData &&
|
||||
other.id == this.id &&
|
||||
other.assetId == this.assetId &&
|
||||
other.action == this.action &&
|
||||
other.parameters == this.parameters);
|
||||
}
|
||||
|
||||
class AssetEditEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.AssetEditEntityData> {
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> assetId;
|
||||
final i0.Value<i2.AssetEditAction> action;
|
||||
final i0.Value<Map<String, Object?>> parameters;
|
||||
const AssetEditEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.assetId = const i0.Value.absent(),
|
||||
this.action = const i0.Value.absent(),
|
||||
this.parameters = const i0.Value.absent(),
|
||||
});
|
||||
AssetEditEntityCompanion.insert({
|
||||
required String id,
|
||||
required String assetId,
|
||||
required i2.AssetEditAction action,
|
||||
required Map<String, Object?> parameters,
|
||||
}) : id = i0.Value(id),
|
||||
assetId = i0.Value(assetId),
|
||||
action = i0.Value(action),
|
||||
parameters = i0.Value(parameters);
|
||||
static i0.Insertable<i1.AssetEditEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? assetId,
|
||||
i0.Expression<int>? action,
|
||||
i0.Expression<i3.Uint8List>? parameters,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (assetId != null) 'asset_id': assetId,
|
||||
if (action != null) 'action': action,
|
||||
if (parameters != null) 'parameters': parameters,
|
||||
});
|
||||
}
|
||||
|
||||
i1.AssetEditEntityCompanion copyWith({
|
||||
i0.Value<String>? id,
|
||||
i0.Value<String>? assetId,
|
||||
i0.Value<i2.AssetEditAction>? action,
|
||||
i0.Value<Map<String, Object?>>? parameters,
|
||||
}) {
|
||||
return i1.AssetEditEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
assetId: assetId ?? this.assetId,
|
||||
action: action ?? this.action,
|
||||
parameters: parameters ?? this.parameters,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<String>(id.value);
|
||||
}
|
||||
if (assetId.present) {
|
||||
map['asset_id'] = i0.Variable<String>(assetId.value);
|
||||
}
|
||||
if (action.present) {
|
||||
map['action'] = i0.Variable<int>(
|
||||
i1.$AssetEditEntityTable.$converteraction.toSql(action.value),
|
||||
);
|
||||
}
|
||||
if (parameters.present) {
|
||||
map['parameters'] = i0.Variable<i3.Uint8List>(
|
||||
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters.value),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('AssetEditEntityCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('action: $action, ')
|
||||
..write('parameters: $parameters')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
||||
assetCount: assetCount,
|
||||
backupSelection: backupSelection,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||
isIosSharedAlbum: isIosSharedAlbum,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
|
||||
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
const LocalAssetEntity();
|
||||
|
||||
@@ -16,6 +17,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
|
||||
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
||||
|
||||
TextColumn get iCloudId => text().nullable()();
|
||||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
|
||||
RealColumn get latitude => real().nullable()();
|
||||
@@ -27,7 +30,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
}
|
||||
|
||||
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
LocalAsset toDto({String? remoteId}) => LocalAsset(
|
||||
LocalAsset toDto({required bool isEdited, String? remoteId}) => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: checksum,
|
||||
@@ -43,5 +46,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
cloudId: iCloudId,
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
i0.Value<String?> iCloudId,
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
@@ -38,6 +39,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
i0.Value<String?> iCloudId,
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
@@ -108,6 +110,11 @@ class $$LocalAssetEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get iCloudId => $composableBuilder(
|
||||
column: $table.iCloudId,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
@@ -188,6 +195,11 @@ class $$LocalAssetEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get iCloudId => $composableBuilder(
|
||||
column: $table.iCloudId,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
@@ -252,6 +264,9 @@ class $$LocalAssetEntityTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<String> get iCloudId =>
|
||||
$composableBuilder(column: $table.iCloudId, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => column,
|
||||
@@ -315,6 +330,7 @@ class $$LocalAssetEntityTableTableManager
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
i0.Value<String?> iCloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
@@ -330,6 +346,7 @@ class $$LocalAssetEntityTableTableManager
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
iCloudId: iCloudId,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
@@ -347,6 +364,7 @@ class $$LocalAssetEntityTableTableManager
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
i0.Value<String?> iCloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
@@ -362,6 +380,7 @@ class $$LocalAssetEntityTableTableManager
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
iCloudId: iCloudId,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
@@ -532,6 +551,17 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
);
|
||||
static const i0.VerificationMeta _iCloudIdMeta = const i0.VerificationMeta(
|
||||
'iCloudId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> iCloudId = i0.GeneratedColumn<String>(
|
||||
'i_cloud_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _adjustmentTimeMeta =
|
||||
const i0.VerificationMeta('adjustmentTime');
|
||||
@override
|
||||
@@ -578,6 +608,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
iCloudId,
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
@@ -661,6 +692,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('i_cloud_id')) {
|
||||
context.handle(
|
||||
_iCloudIdMeta,
|
||||
iCloudId.isAcceptableOrUnknown(data['i_cloud_id']!, _iCloudIdMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('adjustment_time')) {
|
||||
context.handle(
|
||||
_adjustmentTimeMeta,
|
||||
@@ -740,6 +777,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}orientation'],
|
||||
)!,
|
||||
iCloudId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}i_cloud_id'],
|
||||
),
|
||||
adjustmentTime: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}adjustment_time'],
|
||||
@@ -781,6 +822,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
final String? checksum;
|
||||
final bool isFavorite;
|
||||
final int orientation;
|
||||
final String? iCloudId;
|
||||
final DateTime? adjustmentTime;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
@@ -796,6 +838,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
this.checksum,
|
||||
required this.isFavorite,
|
||||
required this.orientation,
|
||||
this.iCloudId,
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
@@ -826,6 +869,9 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
}
|
||||
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||
map['orientation'] = i0.Variable<int>(orientation);
|
||||
if (!nullToAbsent || iCloudId != null) {
|
||||
map['i_cloud_id'] = i0.Variable<String>(iCloudId);
|
||||
}
|
||||
if (!nullToAbsent || adjustmentTime != null) {
|
||||
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime);
|
||||
}
|
||||
@@ -857,6 +903,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||
orientation: serializer.fromJson<int>(json['orientation']),
|
||||
iCloudId: serializer.fromJson<String?>(json['iCloudId']),
|
||||
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
|
||||
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||
@@ -879,6 +926,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
'checksum': serializer.toJson<String?>(checksum),
|
||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||
'orientation': serializer.toJson<int>(orientation),
|
||||
'iCloudId': serializer.toJson<String?>(iCloudId),
|
||||
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
|
||||
'latitude': serializer.toJson<double?>(latitude),
|
||||
'longitude': serializer.toJson<double?>(longitude),
|
||||
@@ -897,6 +945,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
i0.Value<String?> iCloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
@@ -914,6 +963,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
checksum: checksum.present ? checksum.value : this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId,
|
||||
adjustmentTime: adjustmentTime.present
|
||||
? adjustmentTime.value
|
||||
: this.adjustmentTime,
|
||||
@@ -939,6 +989,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
orientation: data.orientation.present
|
||||
? data.orientation.value
|
||||
: this.orientation,
|
||||
iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId,
|
||||
adjustmentTime: data.adjustmentTime.present
|
||||
? data.adjustmentTime.value
|
||||
: this.adjustmentTime,
|
||||
@@ -961,6 +1012,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('iCloudId: $iCloudId, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude')
|
||||
@@ -981,6 +1033,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
iCloudId,
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
@@ -1000,6 +1053,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
other.checksum == this.checksum &&
|
||||
other.isFavorite == this.isFavorite &&
|
||||
other.orientation == this.orientation &&
|
||||
other.iCloudId == this.iCloudId &&
|
||||
other.adjustmentTime == this.adjustmentTime &&
|
||||
other.latitude == this.latitude &&
|
||||
other.longitude == this.longitude);
|
||||
@@ -1018,6 +1072,7 @@ class LocalAssetEntityCompanion
|
||||
final i0.Value<String?> checksum;
|
||||
final i0.Value<bool> isFavorite;
|
||||
final i0.Value<int> orientation;
|
||||
final i0.Value<String?> iCloudId;
|
||||
final i0.Value<DateTime?> adjustmentTime;
|
||||
final i0.Value<double?> latitude;
|
||||
final i0.Value<double?> longitude;
|
||||
@@ -1033,6 +1088,7 @@ class LocalAssetEntityCompanion
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
this.iCloudId = const i0.Value.absent(),
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
@@ -1049,6 +1105,7 @@ class LocalAssetEntityCompanion
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
this.iCloudId = const i0.Value.absent(),
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
@@ -1067,6 +1124,7 @@ class LocalAssetEntityCompanion
|
||||
i0.Expression<String>? checksum,
|
||||
i0.Expression<bool>? isFavorite,
|
||||
i0.Expression<int>? orientation,
|
||||
i0.Expression<String>? iCloudId,
|
||||
i0.Expression<DateTime>? adjustmentTime,
|
||||
i0.Expression<double>? latitude,
|
||||
i0.Expression<double>? longitude,
|
||||
@@ -1083,6 +1141,7 @@ class LocalAssetEntityCompanion
|
||||
if (checksum != null) 'checksum': checksum,
|
||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||
if (orientation != null) 'orientation': orientation,
|
||||
if (iCloudId != null) 'i_cloud_id': iCloudId,
|
||||
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
|
||||
if (latitude != null) 'latitude': latitude,
|
||||
if (longitude != null) 'longitude': longitude,
|
||||
@@ -1101,6 +1160,7 @@ class LocalAssetEntityCompanion
|
||||
i0.Value<String?>? checksum,
|
||||
i0.Value<bool>? isFavorite,
|
||||
i0.Value<int>? orientation,
|
||||
i0.Value<String?>? iCloudId,
|
||||
i0.Value<DateTime?>? adjustmentTime,
|
||||
i0.Value<double?>? latitude,
|
||||
i0.Value<double?>? longitude,
|
||||
@@ -1117,6 +1177,7 @@ class LocalAssetEntityCompanion
|
||||
checksum: checksum ?? this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
iCloudId: iCloudId ?? this.iCloudId,
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
@@ -1161,6 +1222,9 @@ class LocalAssetEntityCompanion
|
||||
if (orientation.present) {
|
||||
map['orientation'] = i0.Variable<int>(orientation.value);
|
||||
}
|
||||
if (iCloudId.present) {
|
||||
map['i_cloud_id'] = i0.Variable<String>(iCloudId.value);
|
||||
}
|
||||
if (adjustmentTime.present) {
|
||||
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime.value);
|
||||
}
|
||||
@@ -1187,6 +1251,7 @@ class LocalAssetEntityCompanion
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('iCloudId: $iCloudId, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude')
|
||||
@@ -1194,3 +1259,8 @@ class LocalAssetEntityCompanion
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
i0.Index get idxLocalAssetCloudId => i0.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'stack.entity.dart';
|
||||
import 'local_asset.entity.dart';
|
||||
import 'local_album.entity.dart';
|
||||
import 'local_album_asset.entity.dart';
|
||||
import 'asset_edit.entity.dart';
|
||||
|
||||
mergedAsset:
|
||||
SELECT
|
||||
@@ -21,7 +22,12 @@ SELECT
|
||||
rae.owner_id,
|
||||
rae.live_photo_video_id,
|
||||
0 as orientation,
|
||||
rae.stack_id
|
||||
rae.stack_id,
|
||||
NULL as i_cloud_id,
|
||||
NULL as latitude,
|
||||
NULL as longitude,
|
||||
NULL as adjustmentTime,
|
||||
CASE WHEN EXISTS (SELECT 1 FROM asset_edit_entity aee WHERE aee.asset_id = rae.id) THEN 1 ELSE 0 END as is_edited
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
@@ -53,7 +59,12 @@ SELECT
|
||||
NULL as owner_id,
|
||||
NULL as live_photo_video_id,
|
||||
lae.orientation,
|
||||
NULL as stack_id
|
||||
NULL as stack_id,
|
||||
lae.i_cloud_id,
|
||||
lae.latitude,
|
||||
lae.longitude,
|
||||
lae.adjustment_time,
|
||||
0 as is_edited
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
||||
@@ -9,10 +9,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i8;
|
||||
|
||||
class MergedAssetDrift extends i1.ModularAccessor {
|
||||
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||
@@ -29,7 +31,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
return customSelect(
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, CASE WHEN EXISTS (SELECT 1 AS _c0 FROM asset_edit_entity AS aee WHERE aee.asset_id = rae.id) THEN 1 ELSE 0 END AS is_edited FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
variables: [
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables,
|
||||
@@ -37,6 +39,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
assetEditEntity,
|
||||
stackEntity,
|
||||
localAlbumAssetEntity,
|
||||
localAlbumEntity,
|
||||
@@ -62,6 +65,11 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
|
||||
orientation: row.read<int>('orientation'),
|
||||
stackId: row.readNullable<String>('stack_id'),
|
||||
iCloudId: row.readNullable<String>('i_cloud_id'),
|
||||
latitude: row.readNullable<double>('latitude'),
|
||||
longitude: row.readNullable<double>('longitude'),
|
||||
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
||||
isEdited: row.read<int>('is_edited'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -103,13 +111,16 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
i3.$LocalAssetEntityTable get localAssetEntity => i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i3.$LocalAssetEntityTable>('local_asset_entity');
|
||||
i6.$LocalAlbumAssetEntityTable get localAlbumAssetEntity =>
|
||||
i6.$AssetEditEntityTable get assetEditEntity => i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i6.$AssetEditEntityTable>('asset_edit_entity');
|
||||
i7.$LocalAlbumAssetEntityTable get localAlbumAssetEntity =>
|
||||
i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i6.$LocalAlbumAssetEntityTable>('local_album_asset_entity');
|
||||
i7.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer(
|
||||
).resultSet<i7.$LocalAlbumAssetEntityTable>('local_album_asset_entity');
|
||||
i8.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i7.$LocalAlbumEntityTable>('local_album_entity');
|
||||
).resultSet<i8.$LocalAlbumEntityTable>('local_album_entity');
|
||||
}
|
||||
|
||||
class MergedAssetResult {
|
||||
@@ -129,6 +140,11 @@ class MergedAssetResult {
|
||||
final String? livePhotoVideoId;
|
||||
final int orientation;
|
||||
final String? stackId;
|
||||
final String? iCloudId;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final DateTime? adjustmentTime;
|
||||
final int isEdited;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
@@ -146,6 +162,11 @@ class MergedAssetResult {
|
||||
this.livePhotoVideoId,
|
||||
required this.orientation,
|
||||
this.stackId,
|
||||
this.iCloudId,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.adjustmentTime,
|
||||
required this.isEdited,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
||||
}
|
||||
|
||||
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
RemoteAsset toDto({String? localId}) => RemoteAsset(
|
||||
RemoteAsset toDto({required bool isEdited, String? localId}) => RemoteAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
ownerId: ownerId,
|
||||
@@ -66,5 +66,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
localId: localId,
|
||||
stackId: stackId,
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin {
|
||||
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get cloudId => text().unique().nullable()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
|
||||
RealColumn get latitude => real().nullable()();
|
||||
|
||||
RealColumn get longitude => real().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {assetId};
|
||||
}
|
||||
827
mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart
generated
Normal file
827
mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart
generated
Normal file
@@ -0,0 +1,827 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart'
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:drift/internal/modular.dart' as i4;
|
||||
|
||||
typedef $$RemoteAssetCloudIdEntityTableCreateCompanionBuilder =
|
||||
i1.RemoteAssetCloudIdEntityCompanion Function({
|
||||
required String assetId,
|
||||
i0.Value<String?> cloudId,
|
||||
i0.Value<DateTime?> createdAt,
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
});
|
||||
typedef $$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder =
|
||||
i1.RemoteAssetCloudIdEntityCompanion Function({
|
||||
i0.Value<String> assetId,
|
||||
i0.Value<String?> cloudId,
|
||||
i0.Value<DateTime?> createdAt,
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
});
|
||||
|
||||
final class $$RemoteAssetCloudIdEntityTableReferences
|
||||
extends
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$RemoteAssetCloudIdEntityTable,
|
||||
i1.RemoteAssetCloudIdEntityData
|
||||
> {
|
||||
$$RemoteAssetCloudIdEntityTableReferences(
|
||||
super.$_db,
|
||||
super.$_table,
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i4.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$RemoteAssetCloudIdEntityTable>(
|
||||
'remote_asset_cloud_id_entity',
|
||||
)
|
||||
.assetId,
|
||||
i4.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||
final $_column = $_itemColumn<String>('asset_id')!;
|
||||
|
||||
final manager = i3
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i4.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $$RemoteAssetCloudIdEntityTableFilterComposer
|
||||
extends
|
||||
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
|
||||
$$RemoteAssetCloudIdEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnFilters<String> get cloudId => $composableBuilder(
|
||||
column: $table.cloudId,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<double> get latitude => $composableBuilder(
|
||||
column: $table.latitude,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<double> get longitude => $composableBuilder(
|
||||
column: $table.longitude,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i3.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$RemoteAssetCloudIdEntityTableOrderingComposer
|
||||
extends
|
||||
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
|
||||
$$RemoteAssetCloudIdEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.ColumnOrderings<String> get cloudId => $composableBuilder(
|
||||
column: $table.cloudId,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<double> get latitude => $composableBuilder(
|
||||
column: $table.latitude,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<double> get longitude => $composableBuilder(
|
||||
column: $table.longitude,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i3.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$RemoteAssetCloudIdEntityTableAnnotationComposer
|
||||
extends
|
||||
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
|
||||
$$RemoteAssetCloudIdEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
i0.GeneratedColumn<String> get cloudId =>
|
||||
$composableBuilder(column: $table.cloudId, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<DateTime> get adjustmentTime => $composableBuilder(
|
||||
column: $table.adjustmentTime,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumn<double> get latitude =>
|
||||
$composableBuilder(column: $table.latitude, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<double> get longitude =>
|
||||
$composableBuilder(column: $table.longitude, builder: (column) => column);
|
||||
|
||||
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.assetId,
|
||||
referencedTable: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i3.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i4.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$RemoteAssetCloudIdEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$RemoteAssetCloudIdEntityTable,
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i1.$$RemoteAssetCloudIdEntityTableFilterComposer,
|
||||
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer,
|
||||
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer,
|
||||
$$RemoteAssetCloudIdEntityTableCreateCompanionBuilder,
|
||||
$$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i1.$$RemoteAssetCloudIdEntityTableReferences,
|
||||
),
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
> {
|
||||
$$RemoteAssetCloudIdEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$RemoteAssetCloudIdEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$RemoteAssetCloudIdEntityTableFilterComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
createComputedFieldComposer: () =>
|
||||
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> assetId = const i0.Value.absent(),
|
||||
i0.Value<String?> cloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
}) => i1.RemoteAssetCloudIdEntityCompanion(
|
||||
assetId: assetId,
|
||||
cloudId: cloudId,
|
||||
createdAt: createdAt,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String assetId,
|
||||
i0.Value<String?> cloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
}) => i1.RemoteAssetCloudIdEntityCompanion.insert(
|
||||
assetId: assetId,
|
||||
cloudId: cloudId,
|
||||
createdAt: createdAt,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
(e) => (
|
||||
e.readTable(table),
|
||||
i1.$$RemoteAssetCloudIdEntityTableReferences(db, table, e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({assetId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins:
|
||||
<
|
||||
T extends i0.TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (assetId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.assetId,
|
||||
referencedTable: i1
|
||||
.$$RemoteAssetCloudIdEntityTableReferences
|
||||
._assetIdTable(db),
|
||||
referencedColumn: i1
|
||||
.$$RemoteAssetCloudIdEntityTableReferences
|
||||
._assetIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$RemoteAssetCloudIdEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$RemoteAssetCloudIdEntityTable,
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i1.$$RemoteAssetCloudIdEntityTableFilterComposer,
|
||||
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer,
|
||||
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer,
|
||||
$$RemoteAssetCloudIdEntityTableCreateCompanionBuilder,
|
||||
$$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i1.$$RemoteAssetCloudIdEntityTableReferences,
|
||||
),
|
||||
i1.RemoteAssetCloudIdEntityData,
|
||||
i0.PrefetchHooks Function({bool assetId})
|
||||
>;
|
||||
|
||||
class $RemoteAssetCloudIdEntityTable extends i2.RemoteAssetCloudIdEntity
|
||||
with
|
||||
i0.TableInfo<
|
||||
$RemoteAssetCloudIdEntityTable,
|
||||
i1.RemoteAssetCloudIdEntityData
|
||||
> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$RemoteAssetCloudIdEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
|
||||
'assetId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
|
||||
'asset_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
|
||||
),
|
||||
);
|
||||
static const i0.VerificationMeta _cloudIdMeta = const i0.VerificationMeta(
|
||||
'cloudId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> cloudId = i0.GeneratedColumn<String>(
|
||||
'cloud_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways('UNIQUE'),
|
||||
);
|
||||
static const i0.VerificationMeta _createdAtMeta = const i0.VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> createdAt =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _adjustmentTimeMeta =
|
||||
const i0.VerificationMeta('adjustmentTime');
|
||||
@override
|
||||
late final i0.GeneratedColumn<DateTime> adjustmentTime =
|
||||
i0.GeneratedColumn<DateTime>(
|
||||
'adjustment_time',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _latitudeMeta = const i0.VerificationMeta(
|
||||
'latitude',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<double> latitude = i0.GeneratedColumn<double>(
|
||||
'latitude',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.double,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _longitudeMeta = const i0.VerificationMeta(
|
||||
'longitude',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<double> longitude = i0.GeneratedColumn<double>(
|
||||
'longitude',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.double,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
assetId,
|
||||
cloudId,
|
||||
createdAt,
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'remote_asset_cloud_id_entity';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.RemoteAssetCloudIdEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('asset_id')) {
|
||||
context.handle(
|
||||
_assetIdMeta,
|
||||
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_assetIdMeta);
|
||||
}
|
||||
if (data.containsKey('cloud_id')) {
|
||||
context.handle(
|
||||
_cloudIdMeta,
|
||||
cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('adjustment_time')) {
|
||||
context.handle(
|
||||
_adjustmentTimeMeta,
|
||||
adjustmentTime.isAcceptableOrUnknown(
|
||||
data['adjustment_time']!,
|
||||
_adjustmentTimeMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('latitude')) {
|
||||
context.handle(
|
||||
_latitudeMeta,
|
||||
latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('longitude')) {
|
||||
context.handle(
|
||||
_longitudeMeta,
|
||||
longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {assetId};
|
||||
@override
|
||||
i1.RemoteAssetCloudIdEntityData map(
|
||||
Map<String, dynamic> data, {
|
||||
String? tablePrefix,
|
||||
}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.RemoteAssetCloudIdEntityData(
|
||||
assetId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}asset_id'],
|
||||
)!,
|
||||
cloudId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}cloud_id'],
|
||||
),
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
),
|
||||
adjustmentTime: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}adjustment_time'],
|
||||
),
|
||||
latitude: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.double,
|
||||
data['${effectivePrefix}latitude'],
|
||||
),
|
||||
longitude: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.double,
|
||||
data['${effectivePrefix}longitude'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$RemoteAssetCloudIdEntityTable createAlias(String alias) {
|
||||
return $RemoteAssetCloudIdEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class RemoteAssetCloudIdEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.RemoteAssetCloudIdEntityData> {
|
||||
final String assetId;
|
||||
final String? cloudId;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? adjustmentTime;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
const RemoteAssetCloudIdEntityData({
|
||||
required this.assetId,
|
||||
this.cloudId,
|
||||
this.createdAt,
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['asset_id'] = i0.Variable<String>(assetId);
|
||||
if (!nullToAbsent || cloudId != null) {
|
||||
map['cloud_id'] = i0.Variable<String>(cloudId);
|
||||
}
|
||||
if (!nullToAbsent || createdAt != null) {
|
||||
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||
}
|
||||
if (!nullToAbsent || adjustmentTime != null) {
|
||||
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime);
|
||||
}
|
||||
if (!nullToAbsent || latitude != null) {
|
||||
map['latitude'] = i0.Variable<double>(latitude);
|
||||
}
|
||||
if (!nullToAbsent || longitude != null) {
|
||||
map['longitude'] = i0.Variable<double>(longitude);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory RemoteAssetCloudIdEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return RemoteAssetCloudIdEntityData(
|
||||
assetId: serializer.fromJson<String>(json['assetId']),
|
||||
cloudId: serializer.fromJson<String?>(json['cloudId']),
|
||||
createdAt: serializer.fromJson<DateTime?>(json['createdAt']),
|
||||
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
|
||||
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'assetId': serializer.toJson<String>(assetId),
|
||||
'cloudId': serializer.toJson<String?>(cloudId),
|
||||
'createdAt': serializer.toJson<DateTime?>(createdAt),
|
||||
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
|
||||
'latitude': serializer.toJson<double?>(latitude),
|
||||
'longitude': serializer.toJson<double?>(longitude),
|
||||
};
|
||||
}
|
||||
|
||||
i1.RemoteAssetCloudIdEntityData copyWith({
|
||||
String? assetId,
|
||||
i0.Value<String?> cloudId = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
}) => i1.RemoteAssetCloudIdEntityData(
|
||||
assetId: assetId ?? this.assetId,
|
||||
cloudId: cloudId.present ? cloudId.value : this.cloudId,
|
||||
createdAt: createdAt.present ? createdAt.value : this.createdAt,
|
||||
adjustmentTime: adjustmentTime.present
|
||||
? adjustmentTime.value
|
||||
: this.adjustmentTime,
|
||||
latitude: latitude.present ? latitude.value : this.latitude,
|
||||
longitude: longitude.present ? longitude.value : this.longitude,
|
||||
);
|
||||
RemoteAssetCloudIdEntityData copyWithCompanion(
|
||||
i1.RemoteAssetCloudIdEntityCompanion data,
|
||||
) {
|
||||
return RemoteAssetCloudIdEntityData(
|
||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||
cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
adjustmentTime: data.adjustmentTime.present
|
||||
? data.adjustmentTime.value
|
||||
: this.adjustmentTime,
|
||||
latitude: data.latitude.present ? data.latitude.value : this.latitude,
|
||||
longitude: data.longitude.present ? data.longitude.value : this.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('RemoteAssetCloudIdEntityData(')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('cloudId: $cloudId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
assetId,
|
||||
cloudId,
|
||||
createdAt,
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.RemoteAssetCloudIdEntityData &&
|
||||
other.assetId == this.assetId &&
|
||||
other.cloudId == this.cloudId &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.adjustmentTime == this.adjustmentTime &&
|
||||
other.latitude == this.latitude &&
|
||||
other.longitude == this.longitude);
|
||||
}
|
||||
|
||||
class RemoteAssetCloudIdEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.RemoteAssetCloudIdEntityData> {
|
||||
final i0.Value<String> assetId;
|
||||
final i0.Value<String?> cloudId;
|
||||
final i0.Value<DateTime?> createdAt;
|
||||
final i0.Value<DateTime?> adjustmentTime;
|
||||
final i0.Value<double?> latitude;
|
||||
final i0.Value<double?> longitude;
|
||||
const RemoteAssetCloudIdEntityCompanion({
|
||||
this.assetId = const i0.Value.absent(),
|
||||
this.cloudId = const i0.Value.absent(),
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
});
|
||||
RemoteAssetCloudIdEntityCompanion.insert({
|
||||
required String assetId,
|
||||
this.cloudId = const i0.Value.absent(),
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
}) : assetId = i0.Value(assetId);
|
||||
static i0.Insertable<i1.RemoteAssetCloudIdEntityData> custom({
|
||||
i0.Expression<String>? assetId,
|
||||
i0.Expression<String>? cloudId,
|
||||
i0.Expression<DateTime>? createdAt,
|
||||
i0.Expression<DateTime>? adjustmentTime,
|
||||
i0.Expression<double>? latitude,
|
||||
i0.Expression<double>? longitude,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (assetId != null) 'asset_id': assetId,
|
||||
if (cloudId != null) 'cloud_id': cloudId,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
|
||||
if (latitude != null) 'latitude': latitude,
|
||||
if (longitude != null) 'longitude': longitude,
|
||||
});
|
||||
}
|
||||
|
||||
i1.RemoteAssetCloudIdEntityCompanion copyWith({
|
||||
i0.Value<String>? assetId,
|
||||
i0.Value<String?>? cloudId,
|
||||
i0.Value<DateTime?>? createdAt,
|
||||
i0.Value<DateTime?>? adjustmentTime,
|
||||
i0.Value<double?>? latitude,
|
||||
i0.Value<double?>? longitude,
|
||||
}) {
|
||||
return i1.RemoteAssetCloudIdEntityCompanion(
|
||||
assetId: assetId ?? this.assetId,
|
||||
cloudId: cloudId ?? this.cloudId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (assetId.present) {
|
||||
map['asset_id'] = i0.Variable<String>(assetId.value);
|
||||
}
|
||||
if (cloudId.present) {
|
||||
map['cloud_id'] = i0.Variable<String>(cloudId.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (adjustmentTime.present) {
|
||||
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime.value);
|
||||
}
|
||||
if (latitude.present) {
|
||||
map['latitude'] = i0.Variable<double>(latitude.value);
|
||||
}
|
||||
if (longitude.present) {
|
||||
map['longitude'] = i0.Variable<double>(longitude.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('RemoteAssetCloudIdEntityCompanion(')
|
||||
..write('assetId: $assetId, ')
|
||||
..write('cloudId: $cloudId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user