mirror of
https://github.com/immich-app/immich.git
synced 2026-01-15 14:33:16 -08:00
Compare commits
104 Commits
fix/ios-ha
...
chore-prev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcfbd301e0 | ||
|
|
80a5444bf4 | ||
|
|
d59ee7d2ae | ||
|
|
7b3a298c6a | ||
|
|
0a62ec7e29 | ||
|
|
21802ab5ba | ||
|
|
56dfdfd033 | ||
|
|
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();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
270
e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts
Normal file
270
e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { expect, 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('Navigate to next asset via button', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||
|
||||
await page.getByLabel('View next asset').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`);
|
||||
});
|
||||
|
||||
test('Navigate to previous asset via button', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||
|
||||
await page.getByLabel('View previous asset').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`);
|
||||
});
|
||||
|
||||
test('Navigate to next asset via keyboard (ArrowRight)', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`);
|
||||
});
|
||||
|
||||
test('Navigate to previous asset via keyboard (ArrowLeft)', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`);
|
||||
});
|
||||
|
||||
test('Navigate forward 5 times via button', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
await page.getByLabel('View next asset').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + i].id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Navigate backward 5 times via button', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
await page.getByLabel('View previous asset').click();
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index - i]);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - i].id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Navigate forward then backward via keyboard', async ({ page }) => {
|
||||
const asset = selectRandom(assets, rng);
|
||||
const index = assets.indexOf(asset);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||
|
||||
// Navigate forward 3 times
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||
}
|
||||
|
||||
// Navigate backward 3 times to return to original
|
||||
for (let i = 2; i >= 0; i--) {
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||
}
|
||||
|
||||
// Verify we're back at the original asset
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||
});
|
||||
|
||||
test('Verify no next button on last asset', async ({ page }) => {
|
||||
const lastAsset = assets.at(-1)!;
|
||||
await page.goto(`/photos/${lastAsset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, lastAsset);
|
||||
|
||||
// Verify next button doesn't exist
|
||||
await expect(page.getByLabel('View next asset')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Verify no previous button on first asset', async ({ page }) => {
|
||||
const firstAsset = assets[0];
|
||||
await page.goto(`/photos/${firstAsset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, firstAsset);
|
||||
|
||||
// Verify previous button doesn't exist
|
||||
await expect(page.getByLabel('View previous asset')).toHaveCount(0);
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { Page, expect, test } from '@playwright/test';
|
||||
import { utils } from 'src/utils';
|
||||
|
||||
function imageLocator(page: Page) {
|
||||
return page.getByAltText('Image taken on').locator('visible=true');
|
||||
return page.getByAltText('Image taken').locator('visible=true');
|
||||
}
|
||||
test.describe('Photo Viewer', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
@@ -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
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')];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -53,12 +55,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 +73,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 +93,7 @@ class LocalAsset extends BaseAsset {
|
||||
LocalAsset copyWith({
|
||||
String? id,
|
||||
String? remoteId,
|
||||
String? cloudId,
|
||||
String? name,
|
||||
String? checksum,
|
||||
AssetType? type,
|
||||
@@ -105,6 +111,7 @@ class LocalAsset extends BaseAsset {
|
||||
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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deleteAssetsV1(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(
|
||||
|
||||
166
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
166
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
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 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,
|
||||
),
|
||||
])..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(),
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
@@ -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()();
|
||||
@@ -43,5 +46,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
cloudId: iCloudId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
);
|
||||
|
||||
@@ -21,7 +21,11 @@ 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
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
@@ -53,7 +57,11 @@ 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
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
||||
@@ -29,7 +29,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 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 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,
|
||||
@@ -62,6 +62,10 @@ 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -129,6 +133,10 @@ class MergedAssetResult {
|
||||
final String? livePhotoVideoId;
|
||||
final int orientation;
|
||||
final String? stackId;
|
||||
final String? iCloudId;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final DateTime? adjustmentTime;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
@@ -146,6 +154,10 @@ class MergedAssetResult {
|
||||
this.livePhotoVideoId,
|
||||
required this.orientation,
|
||||
this.stackId,
|
||||
this.iCloudId,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.adjustmentTime,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,13 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
|
||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
enum TrashOrigin {
|
||||
// do not change this order!
|
||||
localSync,
|
||||
remoteSync,
|
||||
localUser,
|
||||
}
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)')
|
||||
class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
@@ -19,6 +26,8 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity
|
||||
|
||||
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
||||
|
||||
IntColumn get source => intEnum<TrashOrigin>()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id, albumId};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
required i3.TrashOrigin source,
|
||||
});
|
||||
typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i1.TrashedLocalAssetEntityCompanion Function({
|
||||
@@ -37,6 +38,7 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
i0.Value<i3.TrashOrigin> source,
|
||||
});
|
||||
|
||||
class $$TrashedLocalAssetEntityTableFilterComposer
|
||||
@@ -109,6 +111,12 @@ class $$TrashedLocalAssetEntityTableFilterComposer
|
||||
column: $table.orientation,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<i3.TrashOrigin, i3.TrashOrigin, int>
|
||||
get source => $composableBuilder(
|
||||
column: $table.source,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableOrderingComposer
|
||||
@@ -180,6 +188,11 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
|
||||
column: $table.orientation,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get source => $composableBuilder(
|
||||
column: $table.source,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableAnnotationComposer
|
||||
@@ -233,6 +246,9 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
|
||||
column: $table.orientation,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i3.TrashOrigin, int> get source =>
|
||||
$composableBuilder(column: $table.source, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableTableManager
|
||||
@@ -293,6 +309,7 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
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<i3.TrashOrigin> source = const i0.Value.absent(),
|
||||
}) => i1.TrashedLocalAssetEntityCompanion(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -306,6 +323,7 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
source: source,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -321,6 +339,7 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
required i3.TrashOrigin source,
|
||||
}) => i1.TrashedLocalAssetEntityCompanion.insert(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -334,6 +353,7 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
source: source,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
@@ -519,6 +539,17 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
defaultValue: const i4.Constant(0),
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i3.TrashOrigin, int> source =
|
||||
i0.GeneratedColumn<int>(
|
||||
'source',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
).withConverter<i3.TrashOrigin>(
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
name,
|
||||
type,
|
||||
@@ -532,6 +563,7 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
source,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -682,6 +714,12 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}orientation'],
|
||||
)!,
|
||||
source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}source'],
|
||||
)!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -692,6 +730,8 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AssetType, int, int> $convertertype =
|
||||
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
|
||||
static i0.JsonTypeConverter2<i3.TrashOrigin, int, int> $convertersource =
|
||||
const i0.EnumIndexConverter<i3.TrashOrigin>(i3.TrashOrigin.values);
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
@@ -712,6 +752,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
final String? checksum;
|
||||
final bool isFavorite;
|
||||
final int orientation;
|
||||
final i3.TrashOrigin source;
|
||||
const TrashedLocalAssetEntityData({
|
||||
required this.name,
|
||||
required this.type,
|
||||
@@ -725,6 +766,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
this.checksum,
|
||||
required this.isFavorite,
|
||||
required this.orientation,
|
||||
required this.source,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -753,6 +795,11 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
}
|
||||
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||
map['orientation'] = i0.Variable<int>(orientation);
|
||||
{
|
||||
map['source'] = i0.Variable<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -776,6 +823,9 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||
orientation: serializer.fromJson<int>(json['orientation']),
|
||||
source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromJson(
|
||||
serializer.fromJson<int>(json['source']),
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -796,6 +846,9 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
'checksum': serializer.toJson<String?>(checksum),
|
||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||
'orientation': serializer.toJson<int>(orientation),
|
||||
'source': serializer.toJson<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toJson(source),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -812,6 +865,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
i3.TrashOrigin? source,
|
||||
}) => i1.TrashedLocalAssetEntityData(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -827,6 +881,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
checksum: checksum.present ? checksum.value : this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
source: source ?? this.source,
|
||||
);
|
||||
TrashedLocalAssetEntityData copyWithCompanion(
|
||||
i1.TrashedLocalAssetEntityCompanion data,
|
||||
@@ -850,6 +905,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
orientation: data.orientation.present
|
||||
? data.orientation.value
|
||||
: this.orientation,
|
||||
source: data.source.present ? data.source.value : this.source,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -867,7 +923,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
..write('albumId: $albumId, ')
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('source: $source')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -886,6 +943,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
source,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -902,7 +960,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
other.albumId == this.albumId &&
|
||||
other.checksum == this.checksum &&
|
||||
other.isFavorite == this.isFavorite &&
|
||||
other.orientation == this.orientation);
|
||||
other.orientation == this.orientation &&
|
||||
other.source == this.source);
|
||||
}
|
||||
|
||||
class TrashedLocalAssetEntityCompanion
|
||||
@@ -919,6 +978,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
final i0.Value<String?> checksum;
|
||||
final i0.Value<bool> isFavorite;
|
||||
final i0.Value<int> orientation;
|
||||
final i0.Value<i3.TrashOrigin> source;
|
||||
const TrashedLocalAssetEntityCompanion({
|
||||
this.name = const i0.Value.absent(),
|
||||
this.type = const i0.Value.absent(),
|
||||
@@ -932,6 +992,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
this.source = const i0.Value.absent(),
|
||||
});
|
||||
TrashedLocalAssetEntityCompanion.insert({
|
||||
required String name,
|
||||
@@ -946,10 +1007,12 @@ class TrashedLocalAssetEntityCompanion
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
required i3.TrashOrigin source,
|
||||
}) : name = i0.Value(name),
|
||||
type = i0.Value(type),
|
||||
id = i0.Value(id),
|
||||
albumId = i0.Value(albumId);
|
||||
albumId = i0.Value(albumId),
|
||||
source = i0.Value(source);
|
||||
static i0.Insertable<i1.TrashedLocalAssetEntityData> custom({
|
||||
i0.Expression<String>? name,
|
||||
i0.Expression<int>? type,
|
||||
@@ -963,6 +1026,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
i0.Expression<String>? checksum,
|
||||
i0.Expression<bool>? isFavorite,
|
||||
i0.Expression<int>? orientation,
|
||||
i0.Expression<int>? source,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (name != null) 'name': name,
|
||||
@@ -977,6 +1041,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
if (checksum != null) 'checksum': checksum,
|
||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||
if (orientation != null) 'orientation': orientation,
|
||||
if (source != null) 'source': source,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -993,6 +1058,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
i0.Value<String?>? checksum,
|
||||
i0.Value<bool>? isFavorite,
|
||||
i0.Value<int>? orientation,
|
||||
i0.Value<i3.TrashOrigin>? source,
|
||||
}) {
|
||||
return i1.TrashedLocalAssetEntityCompanion(
|
||||
name: name ?? this.name,
|
||||
@@ -1007,6 +1073,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
checksum: checksum ?? this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
source: source ?? this.source,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1051,6 +1118,11 @@ class TrashedLocalAssetEntityCompanion
|
||||
if (orientation.present) {
|
||||
map['orientation'] = i0.Variable<int>(orientation.value);
|
||||
}
|
||||
if (source.present) {
|
||||
map['source'] = i0.Variable<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source.value),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1068,7 +1140,8 @@ class TrashedLocalAssetEntityCompanion
|
||||
..write('albumId: $albumId, ')
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('source: $source')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||
@@ -57,6 +58,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
RemoteAlbumEntity,
|
||||
RemoteAlbumAssetEntity,
|
||||
RemoteAlbumUserEntity,
|
||||
RemoteAssetCloudIdEntity,
|
||||
MemoryEntity,
|
||||
MemoryAssetEntity,
|
||||
StackEntity,
|
||||
@@ -95,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 14;
|
||||
int get schemaVersion => 16;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -190,6 +192,15 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
||||
},
|
||||
from14To15: (m, v15) async {
|
||||
await m.addColumn(v15.trashedLocalAssetEntity, v15.trashedLocalAssetEntity.source);
|
||||
},
|
||||
from15To16: (m, v16) async {
|
||||
// Add i_cloud_id to local and remote asset tables
|
||||
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
|
||||
await m.createIndex(v16.idxLocalAssetCloudId);
|
||||
await m.createTable(v16.remoteAssetCloudIdEntity);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,21 +27,23 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'
|
||||
as i14;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i15;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i16;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
as i17;
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
as i18;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
as i19;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i20;
|
||||
import 'package:drift/internal/modular.dart' as i21;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i21;
|
||||
import 'package:drift/internal/modular.dart' as i22;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
@@ -72,18 +74,20 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
|
||||
.$RemoteAlbumUserEntityTable(this);
|
||||
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
|
||||
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
|
||||
late final i14.$RemoteAssetCloudIdEntityTable remoteAssetCloudIdEntity = i14
|
||||
.$RemoteAssetCloudIdEntityTable(this);
|
||||
late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this);
|
||||
late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16
|
||||
.$MemoryAssetEntityTable(this);
|
||||
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
|
||||
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
|
||||
late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this);
|
||||
late final i18.$AssetFaceEntityTable assetFaceEntity = i18
|
||||
.$AssetFaceEntityTable(this);
|
||||
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
|
||||
late final i19.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i19
|
||||
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
|
||||
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer(
|
||||
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new);
|
||||
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -97,6 +101,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
i4.idxLocalAssetChecksum,
|
||||
i4.idxLocalAssetCloudId,
|
||||
i2.idxRemoteAssetOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
||||
@@ -107,6 +112,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
@@ -114,8 +120,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
i11.idxLatLng,
|
||||
i19.idxTrashedLocalAssetChecksum,
|
||||
i19.idxTrashedLocalAssetAlbum,
|
||||
i20.idxTrashedLocalAssetChecksum,
|
||||
i20.idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
@@ -249,6 +255,18 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i0.TableUpdate('remote_album_user_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate(
|
||||
'remote_asset_cloud_id_entity',
|
||||
kind: i0.UpdateKind.delete,
|
||||
),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'user_entity',
|
||||
@@ -333,18 +351,24 @@ class $DriftManager {
|
||||
);
|
||||
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
|
||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||
i14.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i16.$$PersonEntityTableTableManager get personEntity =>
|
||||
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i18.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager(
|
||||
i14.$$RemoteAssetCloudIdEntityTableTableManager
|
||||
get remoteAssetCloudIdEntity =>
|
||||
i14.$$RemoteAssetCloudIdEntityTableTableManager(
|
||||
_db,
|
||||
_db.remoteAssetCloudIdEntity,
|
||||
);
|
||||
i15.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i17.$$PersonEntityTableTableManager get personEntity =>
|
||||
i17.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i18.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i19.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.trashedLocalAssetEntity,
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -246,6 +246,25 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||
}
|
||||
|
||||
Future<void> updateCloudMapping(Map<String, String> cloudMapping) {
|
||||
if (cloudMapping.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
return _db.batch((batch) {
|
||||
for (final entry in cloudMapping.entries) {
|
||||
final assetId = entry.key;
|
||||
final cloudId = entry.value;
|
||||
|
||||
batch.update(
|
||||
_db.localAssetEntity,
|
||||
LocalAssetEntityCompanion(iCloudId: Value(cloudId)),
|
||||
where: (f) => f.id.equals(assetId),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> Function(Iterable<LocalAsset>) get _upsertAssets =>
|
||||
CurrentPlatform.isIOS ? _upsertAssetsDarwin : _upsertAssetsAndroid;
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
@@ -126,4 +129,85 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<LocalAsset>> getRemovalCandidates(
|
||||
String userId,
|
||||
DateTime cutoffDate, {
|
||||
AssetFilterType filterType = AssetFilterType.all,
|
||||
bool keepFavorites = true,
|
||||
}) async {
|
||||
final iosSharedAlbumAssets = _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.localAlbumEntity.isIosSharedAlbum.equals(true));
|
||||
|
||||
final query = _db.localAssetEntity.select().join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)),
|
||||
]);
|
||||
|
||||
Expression<bool> whereClause =
|
||||
_db.localAssetEntity.createdAt.isSmallerOrEqualValue(cutoffDate) &
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull();
|
||||
|
||||
// Exclude assets that are in iOS shared albums
|
||||
whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(iosSharedAlbumAssets);
|
||||
|
||||
if (filterType == AssetFilterType.photosOnly) {
|
||||
whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.image);
|
||||
} else if (filterType == AssetFilterType.videosOnly) {
|
||||
whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.video);
|
||||
}
|
||||
|
||||
if (keepFavorites) {
|
||||
whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false);
|
||||
}
|
||||
|
||||
query.where(whereClause);
|
||||
|
||||
final rows = await query.get();
|
||||
return rows.map((row) => row.readTable(_db.localAssetEntity).toDto()).toList();
|
||||
}
|
||||
|
||||
Future<List<LocalAsset>> getEmptyCloudIdAssets() {
|
||||
final query = _db.localAssetEntity.select()..where((row) => row.iCloudId.isNull());
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getHashMappingFromCloudId() async {
|
||||
final query =
|
||||
_db.localAssetEntity.selectOnly().join([
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetCloudIdEntity,
|
||||
_db.localAssetEntity.iCloudId.equalsExp(_db.remoteAssetCloudIdEntity.cloudId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetCloudIdEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..addColumns([_db.localAssetEntity.id, _db.remoteAssetEntity.checksum])
|
||||
..where(
|
||||
_db.remoteAssetCloudIdEntity.cloudId.isNotNull() &
|
||||
_db.localAssetEntity.checksum.isNull() &
|
||||
((_db.remoteAssetCloudIdEntity.adjustmentTime.isExp(_db.localAssetEntity.adjustmentTime)) &
|
||||
(_db.remoteAssetCloudIdEntity.latitude.isExp(_db.localAssetEntity.latitude)) &
|
||||
(_db.remoteAssetCloudIdEntity.longitude.isExp(_db.localAssetEntity.longitude)) &
|
||||
(_db.remoteAssetCloudIdEntity.createdAt.isExp(_db.localAssetEntity.createdAt))),
|
||||
);
|
||||
final mapping = await query
|
||||
.map(
|
||||
(row) => (assetId: row.read(_db.localAssetEntity.id)!, checksum: row.read(_db.remoteAssetEntity.checksum)!),
|
||||
)
|
||||
.get();
|
||||
return {for (final entry in mapping) entry.assetId: entry.checksum};
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user